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 )。
- 从未完成的任务会发生什么? 他们妥善处理?
- 例外:应用程序调用了为不同线程编组的接口
- entity framework6异步操作和TranscationScope
- .GetAwaiter()和ConfigureAwait()之间的区别
- 在几乎相同的方法中异步/等待的行为不同
- Xamarin.Forms – BeginInvokeOnMainThread用于异步操作
- 为什么从Async CTP / Release中删除“SwitchTo”?
- 如何使用CancellationToken安全地取消任务并等待Task.WhenAll
- 如果我在IQueryable上使用等待+ ToListAsync()并且未定义为任务,那么它是否正确