I have been attempting to implement a cache which caches the results of an asynchronous method, with the restriction that I only want that method to run once for any particular item in the cache. Many other implementations I have seen allow the method to run multiple times. Although only the first returned value is actually stored, if this is a long running method, this is not desirable for me.
Storing Task<T>
(instead of T
) in the cache, as recommended by most implementers, is a good idea, but I still could not work out how to prevent the method that fetches the value (the valueFactory) from running more than once for any given item. The solution I came up with was to use TaskCompletionSource instead of Task for the items in the cache.
This is what I came up with:
public class AsyncCache<TKey, TValue>
{
private readonly Func<TKey, Task<TValue>> valueFactory;
private readonly ConcurrentDictionary<TKey, TaskCompletionSource<TValue>> completionSourceCache =
new ConcurrentDictionary<TKey, TaskCompletionSource<TValue>>();
public AsyncCache(Func<TKey, Task<TValue>> valueFactory)
{
this.valueFactory = valueFactory;
}
public async Task<TValue> GetItem(TKey key)
{
if (!completionSourceCache.TryAdd(key, new TaskCompletionSource<TValue>()))
{
return await completionSourceCache[key].Task;
}
TaskCompletionSource<TValue> taskSource = completionSourceCache[key];
try
{
var result = await valueFactory(key);
taskSource.SetResult(result);
}
catch (Exception e)
{
taskSource.SetException(e);
}
return await taskSource.Task;
}
}
The idea was that a key is associated with a TaskCompletionSource. The first time a particular item is requested, the TaskCompletionSource will be added to the dictionary for that key. Any subsequent (potentially reentrant) callers would be returned the Task associated with the TaskCompletionSource.
The first caller would then go on to actually call the method, and once the value was determined, this would be associated with the TaskCompletionSource, and anyone waiting on that Task would be able to view the result.
This is my first use of TaskCompletionSource, so please tell me if you think I've missed anything, or done anything wrong.
Note: I am aware of the ParallelExtensionsExtras AsyncCache here: https://blogs.msdn.microsoft.com/pfxteam/2010/04/23/parallelextensionsextras-tour-12-asynccache/
but I only found this after I'd written the above! This is the only other implementation I've found with the semantics described, but since it uses Lazy, it does not await the result of the value factory. I'm not sure if this makes any difference though, i.e. does this mean it is not truly asynchronous, are new threads created, etc.
Task
cannot be re-run. What do you need this cache for? Isn't it the same asTask.WaitAll
? Perhaps you could add a usage example? \$\endgroup\$dict.TryAdd(key, valueFactory(key))
. This would work, but the item is not added to the dictionary until aftervalueFactory(key)
completes. Thus a reentrant thread could call TryAdd a second time, and run valueFactory again. Although only one result would be added to the dictionary, it would mean that valueFactory had run twice for the same key, which is not what I want in this case. \$\endgroup\$pending
dictionary with a lock, and have it map item to timestamp, which would let you timeout and retry ancient requests. Or chain subsequent tasks to depend on result from initial task. \$\endgroup\$