Async threadsafe从MemoryCache获取

我创建了一个使用.NET MemoryCache的异步缓存。 这是代码

 public async Task GetAsync(string key, Func<Task> 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); } 

我认为唯一的缺点是我需要在锁外等待,所以populator不是线程安全的,但是因为await不能驻留在锁内,我想这是最好的方法。 我错过了任何陷阱吗?

更新 :当antoher线程使缓存无效时,Esers应答的版本也是线程安全的

 public async Task GetAsync(string key, Func<Task> populator, TimeSpan expire, object parameters) { if(parameters != null) key += JsonConvert.SerializeObject(parameters); var lazy = new Lazy<Task>(populator, true); _cache.AddOrGetExisting(key, lazy, DateTimeOffset.Now.Add(expire)); return ((Lazy<Task>) _cache.Get(key)).Value; } 

然而它可能会更慢,因为它创建了永远不会被执行的Lazy实例,并且它在完全线程安全模式下使用Lazy LazyThreadSafetyMode.ExecutionAndPublication

使用新基准更新(更高更好)

 Lazy with lock 42535929 Lazy with GetOrAdd 41070320 (Only solution that is completely thread safe) Semaphore 64573360 

一个简单的解决方案是使用SemaphoreSlim.WaitAsync()而不是锁,然后你可以解决等待锁内的问题。 虽然MemoryCache所有其他方法都是线程安全的。

 private SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1); public async Task GetAsync( string key, Func> 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); } 

虽然已经有一个已经接受的答案,但我会发布一个新的Lazy方法。 想法是: 最小化lock块的持续时间 ,如果缓存中不存在密钥,则将Lazy置于缓存中。 这样,同时使用相同密钥的所有线程将等待相同的Lazy

 public Task GetAsync(string key, Func> populator, TimeSpan expire, object parameters) { if (parameters != null) key += JsonConvert.SerializeObject(parameters); lock (_cache) { if (!_cache.Contains(key)) { var lazy = new Lazy>(populator, true); _cache.Add(key, lazy, DateTimeOffset.Now.Add(expire)); } } return ((Lazy>)_cache.Get(key)).Value; } 

版本2

 public Task GetAsync(string key, Func> populator, TimeSpan expire, object parameters) { if (parameters != null) key += JsonConvert.SerializeObject(parameters); var lazy = ((Lazy>)_cache.Get(key)); if (lazy != null) return lazy.Value; lock (_cache) { if (!_cache.Contains(key)) { lazy = new Lazy>(populator, true); _cache.Add(key, lazy, DateTimeOffset.Now.Add(expire)); return lazy.Value; } return ((Lazy>)_cache.Get(key)).Value; } } 

版本3

 public Task GetAsync(string key, Func> populator, TimeSpan expire, object parameters) { if (parameters != null) key += JsonConvert.SerializeObject(parameters); var task = (Task)_cache.Get(key); if (task != null) return task; var value = populator(); return (Task)_cache.AddOrGetExisting(key, value, DateTimeOffset.Now.Add(expire)) ?? value; } 

当前的答案使用有点过时的System.Runtime.Caching.MemoryCache 。 它们还包含微妙的竞争条件(见评论)。 最后,并非所有这些都允许超时取决于要缓存的值。

这是我尝试使用新的Microsoft.Extensions.Caching.Memory (由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 GetOrAddAsync( string key, Func> factory, Func 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; }); } 

样品用法:

 await GetOrAddAsync("foo", () => Task.Run(() => 42), i => TimeSpan.FromMilliseconds(i))); 

请注意,不能保证只调用一次工厂方法(请参阅https://github.com/aspnet/Caching/issues/240 )。