Reputation: 472
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
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
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