Skip to content
Mike Evans-Larah By Mike Evans-Larah Software Engineer III
Using Lazy and ConcurrentDictionary to ensure a thread-safe, run-once, lazy-loaded collection

Since .NET 4.0, the ConcurrentDictionary<TKey, TValue> collection type has been available as a way of ensuring thread-safe access to a collection of key/value pairs, using fine grained locking. The collection also has AddOrUpdate and GetOrAdd methods that accept delegate value factories, so that you can lazily load your values only when you need to initialize them.

The fine grained locking doesn't apply to these methods that accept a delegate, because trying to lock around an unknown method could potentially cause issues, and as such the code in them is not guaranteed to be atomic. In this case, if multiple threads request the same key/value pair at once, the method may run more than once, and all but one of the results will be thrown away, guaranteeing your value result is the same object across all threads.

However, you may find that you want use the GetOrAdd with delegate overload (to benefit from the lazy initialization and thread-safe result) but also want to ensure that the delegate method only runs once. For example, your delegate method may cause side-effects, or consumes a resource (like network bandwidth) that you want to minimise the impact of.

The Introduction to Rx.NET 2nd Edition (2024) Book, by Ian Griffiths & Lee Campbell, is now available to download for FREE.

In this case, you can use another type available since .NET 4.0 - Lazy<T>. You can wrap up a type such as Lazy<SomeBigObject> with a value factory delegate, and the delegate method will only be run when you call the Value property of the Lazy object.

var lazyBigObject = new Lazy<SomeBigObject>(() => new SomeBigObject());

// Big object only gets created here
var bigObject = lazyBigObject.Value;

Additionally, you can also specify a LazyThreadSafetyMode to the constructor, the default value of which is **ExecutionAndPublication **that ensures only one thread can initialise the value, which is exactly what we want. So we can combine **ConcurrentDictionary **and Lazy to get the behaviour we want. I created a **LazyConcurrentDictionary **to wrap up the **GetOrAdd **method:

public class LazyConcurrentDictionary<TKey, TValue>
{
    private readonly ConcurrentDictionary<TKey, Lazy<TValue>> concurrentDictionary;

    public LazyConcurrentDictionary()
    {
        this.concurrentDictionary = new ConcurrentDictionary<TKey, Lazy<TValue>>();
    }

    public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
    {
        var lazyResult = this.concurrentDictionary.GetOrAdd(key, k => new Lazy<TValue>(() => valueFactory(k), LazyThreadSafetyMode.ExecutionAndPublication));

        return lazyResult.Value;
    }
}
Programming C# 12 Book, by Ian Griffiths, published by O'Reilly Media, is now available to buy.

Now what happens is that if multiple concurrent threads try to call GetOrAdd with the same key at once, multiple Lazy objects may be created but these are cheap, and all but one will be thrown away. The return Lazy object will be the same across all threads, and the first one to call the Value property will run the expensive delegate method, whilst the other threads are locked, waiting for the result.

This ensures that the values in the collection are lazy-loaded but they are guaranteed to only be initialized once.

Example is available to look at over at DotNetFiddle. If you look at the console output, you will see the ConcurrentDictionary executing it's delegate method at least once and the LazyConcurrentDictionary just running it once (you might have to run it a few times to see the regular ConcurrentDictionary execute the method multiple times, but the LazyConcurrentDictionary will always only ever run it once).

Mike Evans-Larah

Software Engineer III

Mike Evans-Larah

Mike is a Software Engineer at endjin with over a decade of experience in solving business problems with technology. He has worked on a wide range of projects for clients across industries such as financial services, recruitment, and retail, with a strong focus on Azure technologies.