Thursday, 24 January 2013

Be aware of exception caching when using System.Lazy<T> to initialise shared objects

System.Lazy<T> gives us a nice declarative way of lazily creating object instances. The class is useful for a couple of reasons.

Firstly, the wrapped object is only initialised (lazily) when the Value property is accessed, allowing work to be deferred until it is actually needed, and could be used to improve an application's start up time. Quoting from this Martin Fowler article is mandatory here:
As many people know, if you're lazy about doing things you'll win when it turns out you don't need to do them at all.
In addition, Lazy<T> can be configured to control initialisation of the value when the Value property is accessed concurrently by multiple threads. This prevents nasty bugs from arising due to different threads getting different copies of an object, or multiple threads running non-thread-safe code concurrently.

Lazy<T>'s thread safe behaviour makes it ideal for managing the initialisation of an object that is costly to create. A trivial example is shown below:


Here are some sort-of-real-world example scenarios where we might consider using a thread-safe Lazy<T> to initialise a shared object within our application:

  • Loading some information from a database when the application starts and storing it for use throughout the application
  • Working with a third party library or framework that requires you to maintain a single application-wide instance of an object instance. For example, it is intended that single NHibernate Configuration and ISessionFactory instances are shared by all threads within an application.
If you are doing something similar to the above and are thinking of using Lazy, then you need to be aware of its thread safety modes and exception caching mechanism.

First, let's look at thread safety in more detail. To illustrate the thread safety options, here are some examples of how you can create instances of Lazy<T>:

There are three LazyThreadSafetyModes that you can choose from:

  • None - The Lazy<T> instance is not thread-safe and multiple threads can initialise the value concurrently. Returning to the example scenarios above, this would result in multiple threads running code to connect to the database or several NHibernate Configuration and ISessionFactory instances being created if a web application were under load.
  • ExecutionAndPublication - Locking is used to ensure that only a single thread initialises the value. Any other concurrent threads accessing the Value property will wait and receive the value initialised by the chosen thread. 
  • PublicationOnly - This one sounds quite fun... If the value has not yet been initialised, all threads that access the Value property concurrently get to have a go at initialising the value. The first to complete is the "winner" and all other threads accessing the Lazy<T>'s Value property will get the value initialised by the winning thread. 

ExecutionAndPublication is the most useful mode if you want to ensure that only a single thread is allowed to execute some costly initialisation functionality. As we saw in the Lazy<T> constructor examples above, Lazy<T> uses this thread safety mode by default, unless you specify another mode explicitly.

So far, so good. But what happens if something goes wrong during initialisation of a lazy value and an exception is thrown? The way that exceptions are dealt with varies according to thread safety mode. This is the full description of ExecutionAndPublication from MSDN (with my emphasis on exception caching):
Locks are used to ensure that only a single thread can initialize a Lazy<T> instance in a thread-safe manner. If the initialization method (or the default constructor, if there is no initialization method) uses locks internally, deadlocks can occur. If you use a Lazy<T> constructor that specifies an initialization method (valueFactory parameter), and if that initialization method throws an exception (or fails to handle an exception) the first time you call the Lazy<T>.Value property, then the exception is cached and thrown again on subsequent calls to the Lazy<T>.Value property. If you use a Lazy<T> constructor that does not specify an initialization method, exceptions that are thrown by the default constructor for T are not cached. In that case, a subsequent call to the Lazy<T>.Value property might successfully initialize the Lazy<T> instance. If the initialization method recursively accesses the Value property of the Lazy<T> instance, an InvalidOperationException is thrown.
This is quite subtle and I'm glad I RTFMed this.

To clarify:

  • If you create your Lazy<T> using an initialisation method (i.e., a "valueFactory" parameter), e.g. var lazy = new Lazy<T>(() => Initialise()) and that delegate throws an exception, no further attempts will be made to initialise the value. The original exception will be thrown every time the Value property is accessed. 
  • If you don't use a delegate (i.e., you are specifying that Lazy<T> will initialise an instance of target type T using its default constructor), an exception thrown during initialisation will not be cached and further attempts will be made to create the value until initialisation succeeds.

Let's consider this in the context of our sort-of-real-world example scenarios.

Firstly, imagine a web application that uses a Lazy<T> with an initialisation method that loads data from a database to initialise an object. If an exception were thrown by the initialisation method, perhaps due to a temporary connectivity issue, the same exception would be thrown every time the lazy value were accessed. Every request that required access to the lazy value would result in a server error.

The second scenario, using Lazy<T> to initialise an NHibernate Configuration or ISessionFactory, could also potentially cripple the application for similar reasons. An exception will be thrown if the database specified in the configuration is not available when Configuration.BuildSessionFactory is called and no further attempts would be made to build the session factory, even when the database became available.

So, let's conclude with ... a top tip: You must not use a thread-safe (ExecutionAndPublication) Lazy<T> with a initialisation method ("valueFactory" parameter) if there is any risk of the method failing. If it fails, it won't get the chance to run again, which could bring down your application.

If this helps just one or two developers out there I'll be happy. Let me know if this has helped you!

Some alternatives and work-arounds will be explored in a future post.

2 comments:

  1. Thanks a lot for sharing this with all of us, I like it and we can communicate. Do you need buy app ratings and reviews. To boost app ranking and double app downloads now.

    ReplyDelete