Does it make sense to use Lazy <T> in this code example?

One of the use cases Task

I found on MSDN (found here) seems pretty strange. Is there any reason for the class Lazy<T>

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

      

Once created Lazy<Task<TValue>>

, it is immediately accessed. If it is immediately accessed, then use Lazy<T>

only adds overhead and makes this example more confusing than necessary. If I don't see something here?

+3


source to share


2 answers


You are correct that it is created and then immediately accessed it, but it is important to note that you do not always use the created object .

The dictionary function GetOrAdd

acts like a Lazy<T>

c LazyThreadSafetyMode.PublicationOnly

, which means the delegate you pass as the factory function may not be executed once, but only the first one to finish will be returned to all callers.

The Lazy<T>

default behavior is LazyThreadSafetyMode.ExecutionAndPublication

, which means that the first person to call the factory function will acquire the lock and any other callers must wait for the factory function to complete before continuing.

If we reformat the get method, it becomes a little clearer.



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 call this[Tkey key]

at the same time at the same time and both reach GetOrAdd

without a value in the dictionary with the passed key, then it new Lazy<Task<TValue>>(() => _valueFactory(toAdd))

will execute twice , however only one of them needs to be returned to both calls. It doesn't matter because it _valueFactory

hasn't been completed yet and that's the expensive part, all we do is make a new one Lasy<T>

.

After the call returns, GetOrAdd

you will always be working with the same single object, that is, when called .Value

, it uses the mode ExecutionAndPublication

and blocks any other calls .Value

until _valueFactory

the tokens are executed.

If we didn't use it Lazt<T>

, we _valueFactory

would execute multiple times before returning a single result.

+4


source


ConcurrentDictionary

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

Several laziness can be created, but only one of them will actually be implemented, because the dictionary only ever returns one of them.



This guarantees a guaranteed one-time execution. This is a standard template.

+1


source







All Articles