Caching asynchronous operations Caching asynchronous operations multithreading multithreading

Caching asynchronous operations


First of all, both your approaches are wrong, because they don't save you any requests (though the second one at least saves you time).

Your first code (the one with await) does this:

  1. Make the request.
  2. Wait for the request to complete.
  3. If there already was a result in the cache, ignore the result of the request.

Your second code removes step 2, so it's faster, but you're still making lots of unnecessary requests.

What you should do instead is to use the overload of GetOrAdd() that takes a delegate:

public Task<String> GetStuffAsync(String url){    return _cache.GetOrAdd(url, GetStuffInternalAsync);}

This doesn't completely eliminate the possibility of requests that are ignored, but it does make them much less likely. (For that, you could try canceling requests that you know are being ignored, but I don't think that's worth the effort here.)


Now to your actual question. What I think you should do is to use the AddOrUpdate() method. If the value isn't there yet, add it. If it's there, replace it if it's faulted:

public Task<String> GetStuffAsync(String url){    return _cache.AddOrUpdate(        url, GetStuffInternalAsync, (u, task) =>        {            if (task.IsCanceled || task.IsFaulted)                return GetStuffInternalAsync(u);            return task;        });}


It's actually reasonable (and depending on your design and performance, crucial) to keep those failed tasks as a Negative Cache. Otherwise, if a url always fails, using it again and again defeats the point of using a cache altogether.

What you do need is a way to clear the cache from time to time. The simplest way is to have a timer that replaces the ConcurrentDictionarry instance. The more robust solution is to build your own LruDictionary or something similar.


Another easy way to do this is to extend Lazy<T> to be AsyncLazy<T>, like so:

public class AsyncLazy<T> : Lazy<Task<T>>{    public AsyncLazy(Func<Task<T>> taskFactory, LazyThreadSafetyMode mode) :        base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap(), mode)    { }    public TaskAwaiter<T> GetAwaiter() { return Value.GetAwaiter(); }}

Then you can do this:

private readonly ConcurrentDictionary<string, AsyncLazy<string>> _cache    = new ConcurrentDictionary<string, AsyncLazy<string>>();public async Task<string> GetStuffAsync(string url){    return await _cache.GetOrAdd(url,        new AsyncLazy<string>(            () => GetStuffInternalAsync(url),            LazyThreadSafetyMode.ExecutionAndPublication));}