BetaKeja
BetaKeja

Reputation: 472

Is there any purpose to Lazy<T> in this sample code?

One of the examples for using Task I found on MSDN (found here) seems rather odd. Is there any reason for the Lazy<T> class to be used here?

public class AsyncCache<TKey, TValue>
{
    private readonly Func<TKey, Task<TValue>> _valueFactory;
    private readonly ConcurrentDictionary<TKey, Lazy<Task<TValue>>> _map;

    public AsyncCache(Func<TKey, Task<TValue>> valueFactory)
    {
        if (valueFactory == null) throw new ArgumentNullException("loader");
        _valueFactory = valueFactory;
        _map = new ConcurrentDictionary<TKey, Lazy<Task<TValue>>>();
    }

    public Task<TValue> this[TKey key]
    {
        get
        {
            if (key == null) throw new ArgumentNullException("key");
            return _map.GetOrAdd(key, toAdd => 
                new Lazy<Task<TValue>>(() => _valueFactory(toAdd))).Value;
        }
    }
}

As soon as the Lazy<Task<TValue>> is created it is immediately accessed. If it's being accessed immediately then using Lazy<T> only adds overhead and is making this example more confusing than it needs to. Unless I am missing something here?

Upvotes: 3

Views: 107

Answers (2)

Scott Chamberlain
Scott Chamberlain

Reputation: 127553

You are correct that it is created then immediately accessed, but the important thing to note is you don't always use the object you create.

Dictionary's GetOrAdd function acts like a Lazy<T> with LazyThreadSafetyMode.PublicationOnly which means the delegate you pass in as the factory function may be executed more than once but only the first to finish will be returned to all callers.

The default behavior of Lazy<T> is LazyThreadSafetyMode.ExecutionAndPublication which means the first person to call the factory function will obtain a lock and any other callers have to wait till the factory function finishes before continuing on.

If we reformat the get method it becomes a little more clear.

public Task<TValue> this[TKey key]
{
    get
    {
        if (key == null) 
           throw new ArgumentNullException("key");
        var cacheItem = _map.GetOrAdd(key, toAdd => 
                             new Lazy<Task<TValue>>(() => _valueFactory(toAdd)));
        return cacheItem.Value;
    }
}

So if two threads both call this[Tkey key] at the same time and they both reach GetOrAdd with no value in the dictionary with the passed in key then new Lazy<Task<TValue>>(() => _valueFactory(toAdd)) will be executed twice, however only the first one to complete gets returned to both calls. This is not a big deal because _valueFactory is not executed yet and that is the expensive portion, all we are doing is making a new Lasy<T>.

Once the GetOrAdd call returns you will always be working with the same single object, that is when .Value is called, this uses ExecutionAndPublication mode and will block any other calls to .Value until _valueFactory finshes executing.

If we did not use the Lazt<T> then _valueFactory would have gotten executed multiple times before a single result was returned.

Upvotes: 4

usr
usr

Reputation: 171178

ConcurrentDictionary does not guarantee that your value factory executes only once. This is in order to avoid calling user code under an internal lock. This can lead to deadlocks which is bad API design.

Multiple lazies can be created but only one of them will actually be materialized because the dictionary only ever returns one of them.

This ensures guaranteed one-time execution. It's a standard pattern.

Upvotes: 1

Related Questions