I have created a async cache that uses .NET MemoryCache
underneath.
This is the code:
public async Task<T> GetAsync(string key, Func<Task<T>> populator, TimeSpan expire, object parameters)
{
if(parameters != null)
key += JsonConvert.SerializeObject(parameters);
if(!_cache.Contains(key))
{
var data = await populator();
lock(_cache)
{
if(!_cache.Contains(key)) //Check again but locked this time
_cache.Add(key, data, DateTimeOffset.Now.Add(expire));
}
}
return (T)_cache.Get(key);
}
I think the only downside is that I need to do the await outside the lock so the populator isn't thread safe, but since the await can't reside inside a lock I guess this is the best way. Are there any pitfalls that I have missed?
Update: A version of Esers answer that is also threadsafe when another thread invalidates the cache:
public async Task<T> GetAsync(string key, Func<Task<T>> populator, TimeSpan expire, object parameters)
{
if(parameters != null)
key += JsonConvert.SerializeObject(parameters);
var lazy = new Lazy<Task<T>>(populator, true);
_cache.AddOrGetExisting(key, lazy, DateTimeOffset.Now.Add(expire));
return ((Lazy<Task<T>>) _cache.Get(key)).Value;
}
It can however be slower because it creates Lazy instances that never will be executed and it uses Lazy in full threadsafe mode LazyThreadSafetyMode.ExecutionAndPublication
Update with new benchmark (Higher is better)
Lazy with lock 42535929
Lazy with GetOrAdd 41070320 (Only solution that is completely thread safe)
Semaphore 64573360
MemoryCache is threadsafe. Multiple concurrent threads can read and write a MemoryCache instance. Internally thread-safety is automatically handled to ensure the cache is updated in a consistent manner.
Note that the MemoryCache is a singleton, but within the process. It is not (yet) a DistributedCache. Also note that Caching is Complex(tm) and that thousands of pages have been written about caching by smart people. This is a blog post as part of a series, so use your head and do your research.
An in-memory cache removes the performance delays when an application built on a disk-based database must retrieve data from a disk before processing. Reading data from memory is faster than from the disk. In-memory caching avoids latency and improves online application performance.
In-Memory Cache is used for when you want to implement cache in a single process. When the process dies, the cache dies with it. If you're running the same process on several servers, you will have a separate cache for each server. Persistent in-process Cache is when you back up your cache outside of process memory.
A simple solution would be to use SemaphoreSlim.WaitAsync()
instead of a lock, and then you could get around the issue of awaiting inside a lock. Although, all other methods of MemoryCache
are thread-safe.
private SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1);
public async Task<T> GetAsync(
string key, Func<Task<T>> populator, TimeSpan expire, object parameters)
{
if (parameters != null)
key += JsonConvert.SerializeObject(parameters);
if (!_cache.Contains(key))
{
await semaphoreSlim.WaitAsync();
try
{
if (!_cache.Contains(key))
{
var data = await populator();
_cache.Add(key, data, DateTimeOffset.Now.Add(expire));
}
}
finally
{
semaphoreSlim.Release();
}
}
return (T)_cache.Get(key);
}
The current answers use the somewhat outdated System.Runtime.Caching.MemoryCache
. They also contain subtle race conditions (see comments). Finally, not all of them allow the timeout to be dependent on the value to be cached.
Here's my attempt using the new Microsoft.Extensions.Caching.Memory (used by ASP.NET Core):
//Add NuGet package: Microsoft.Extensions.Caching.Memory
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Primitives;
MemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
public Task<T> GetOrAddAsync<T>(
string key, Func<Task<T>> factory, Func<T, TimeSpan> expirationCalculator)
{
return _cache.GetOrCreateAsync(key, async cacheEntry =>
{
var cts = new CancellationTokenSource();
cacheEntry.AddExpirationToken(new CancellationChangeToken(cts.Token));
var value = await factory().ConfigureAwait(false);
cts.CancelAfter(expirationCalculator(value));
return value;
});
}
Sample usage:
await GetOrAddAsync("foo", () => Task.Run(() => 42), i => TimeSpan.FromMilliseconds(i)));
Note that it is not guaranteed for the factory method to be called only once (see https://github.com/aspnet/Caching/issues/240).
Although there is an already accepted answer, I'll post a new one with Lazy<T>
approach. Idea is: to minimize the duration of lock
block, if the key doesn't exists in cache, put a Lazy<T>
to cache. That way all threads using the same key at the same time will be waiting the same Lazy<T>
's value
public Task<T> GetAsync<T>(string key, Func<Task<T>> populator, TimeSpan expire, object parameters)
{
if (parameters != null)
key += JsonConvert.SerializeObject(parameters);
lock (_cache)
{
if (!_cache.Contains(key))
{
var lazy = new Lazy<Task<T>>(populator, true);
_cache.Add(key, lazy, DateTimeOffset.Now.Add(expire));
}
}
return ((Lazy<Task<T>>)_cache.Get(key)).Value;
}
Version2
public Task<T> GetAsync<T>(string key, Func<Task<T>> populator, TimeSpan expire, object parameters)
{
if (parameters != null)
key += JsonConvert.SerializeObject(parameters);
var lazy = ((Lazy<Task<T>>)_cache.Get(key));
if (lazy != null) return lazy.Value;
lock (_cache)
{
if (!_cache.Contains(key))
{
lazy = new Lazy<Task<T>>(populator, true);
_cache.Add(key, lazy, DateTimeOffset.Now.Add(expire));
return lazy.Value;
}
return ((Lazy<Task<T>>)_cache.Get(key)).Value;
}
}
Version3
public Task<T> GetAsync<T>(string key, Func<Task<T>> populator, TimeSpan expire, object parameters)
{
if (parameters != null)
key += JsonConvert.SerializeObject(parameters);
var task = (Task<T>)_cache.Get(key);
if (task != null) return task;
var value = populator();
return
(Task<T>)_cache.AddOrGetExisting(key, value, DateTimeOffset.Now.Add(expire)) ?? value;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With