Search code examples
c#asynchronouslazy-initialization

Purpose of Lazy<T> on this MSDN Example


I've been reading about this Asynchronous article from MSDN, but I cannot understand the purpose of the Lazy<T> on the given example.

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;
        }
    }
}

From what I understand, when you call .Value of Lazy<T> then it will call the constructor within. From the example, it is called right away, so why add Lazy<T>?


Solution

  • Suppose you modified it to not use Lazy<T>.

    public class AsyncCache<TKey, TValue>
    {
        private readonly Func<TKey, Task<TValue>> _valueFactory;
        private readonly ConcurrentDictionary<TKey, Task<TValue>> _map;
    
        public AsyncCache(Func<TKey, Task<TValue>> valueFactory)
        {
            if (valueFactory == null) throw new ArgumentNullException("loader");
            _valueFactory = valueFactory;
            _map = new ConcurrentDictionary<TKey, Task<TValue>>();
        }
    
        public Task<TValue> this[TKey key]
        {
            get
            {
                if (key == null) throw new ArgumentNullException("key");
                return _map.GetOrAdd(key, toAdd => _valueFactory(toAdd));
            }
        }
    }
    

    See the remarks in the documentation:

    If you call GetOrAdd simultaneously on different threads, addValueFactory may be called multiple times, but its key/value pair might not be added to the dictionary for every call.

    So the _valueFactory might be called multiple times for the same key if multiple accesses to the same key occur simultaneously.

    Now how does the use of Lazy<T> fix the problem? Although multiple Lazy<Task<TValue>> instances might be created by concurrent calls, only one will ever be returned from GetOrAdd. So only one will ever have its Value property accessed. So only one call to _valueFactory will ever occur per key.

    This is of course a desirable feature. If I made an AsyncCache<string, byte[]> cache that was created with the lambda url => DownloadFile(url), I wouldn't want a pile of concurrent requests to cache[myUrl] to download the file multiple times.