C# 异步线程安全从MemoryCache获取
我创建了一个异步缓存,它在下面使用.NETC# 异步线程安全从MemoryCache获取,c#,thread-safety,async-await,C#,Thread Safety,Async Await,我创建了一个异步缓存,它在下面使用.NETMemoryCache。 代码如下: public async Task<T> GetAsync(string key, Func<Task<T>> populator, TimeSpan expire, object parameters) { if(parameters != null) key += JsonConvert.SerializeObject(parameters);
MemoryCache
。
代码如下:
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);
}
一个简单的解决方案是使用而不是锁,然后您可以绕过在锁中等待的问题。尽管如此,
MemoryCache
的所有其他方法都是线程安全的
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);
}
private-SemaphoreSlim-SemaphoreSlim=new-SemaphoreSlim(1);
公共异步任务GetAsync(
字符串键、函数填充器、TimeSpan过期、对象参数)
{
if(参数!=null)
key+=JsonConvert.SerializeObject(参数);
如果(!\u缓存包含(键))
{
等待信号量lim.WaitAsync();
尝试
{
如果(!\u缓存包含(键))
{
var data=await populator();
_Add(key,data,DateTimeOffset.Now.Add(expire));
}
}
最后
{
semaphoreSlim.Release();
}
}
return(T)_cache.Get(key);
}
虽然有一个已经被接受的答案,但我将用惰性方法发布一个新的答案。想法是:为了最小化lock
块的持续时间,如果缓存中不存在密钥,则将Lazy
放入缓存。这样,所有同时使用同一个键的线程都将等待相同的Lazy
值
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;
}
公共任务GetAsync(字符串键、函数填充器、TimeSpan过期、对象参数)
{
if(参数!=null)
key+=JsonConvert.SerializeObject(参数);
锁(_缓存)
{
如果(!\u缓存包含(键))
{
var lazy=new lazy(populator,true);
_Add(key,lazy,DateTimeOffset.Now.Add(expire));
}
}
返回((Lazy)u 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;
}
}
公共任务GetAsync(字符串键、函数填充器、TimeSpan过期、对象参数)
{
if(参数!=null)
key+=JsonConvert.SerializeObject(参数);
var lazy=((lazy)u cache.Get(key));
if(lazy!=null)返回lazy.Value;
锁(_缓存)
{
如果(!\u缓存包含(键))
{
lazy=新的lazy(populator,true);
_Add(key,lazy,DateTimeOffset.Now.Add(expire));
返回lazy.Value;
}
返回((Lazy)u cache.Get(key)).Value;
}
}
版本3
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;
}
公共任务GetAsync(字符串键、函数填充器、TimeSpan过期、对象参数)
{
if(参数!=null)
key+=JsonConvert.SerializeObject(参数);
var task=(task)\ u cache.Get(key);
如果(task!=null)返回任务;
var值=populator();
返回
(任务)_cache.AddOrGetExisting(key、value、DateTimeOffset.Now.Add(expire))??值;
}
当前的答案使用了有点过时的
System.Runtime.Caching.MemoryCache
。它们还包含微妙的种族条件(参见注释)。最后,并不是所有的缓存都允许超时依赖于要缓存的值
下面是我尝试使用新的(由ASP.NET Core使用的):
请注意,不能保证只调用一次factory方法(请参阅)。这是对Eser(Version2)的尝试性改进。默认情况下,该类是线程安全的,因此可以删除
锁。可能会为给定的键创建多个惰性
对象,但只有一个对象会查询其值
属性,从而导致繁重的任务开始。另一个Lazy
s将保持未使用状态,并将超出范围,很快被垃圾收集
第一个重载是灵活的通用重载,它接受Func
参数。对于最常见的绝对过期和滑动过期,我又增加了两个重载。为了方便起见,可以添加更多的重载
using System.Runtime.Caching;
static partial class MemoryCacheExtensions
{
public static Task<T> GetOrCreateLazyAsync<T>(this MemoryCache cache, string key,
Func<Task<T>> valueFactory, Func<CacheItemPolicy> cacheItemPolicyFactory = null)
{
var lazyTask = (Lazy<Task<T>>)cache.Get(key);
if (lazyTask == null)
{
var newLazyTask = new Lazy<Task<T>>(valueFactory);
var cacheItem = new CacheItem(key, newLazyTask);
var cacheItemPolicy = cacheItemPolicyFactory?.Invoke();
var existingCacheItem = cache.AddOrGetExisting(cacheItem, cacheItemPolicy);
lazyTask = (Lazy<Task<T>>)existingCacheItem?.Value ?? newLazyTask;
}
return ToAsyncConditional(lazyTask.Value);
}
private static Task<TResult> ToAsyncConditional<TResult>(Task<TResult> task)
{
if (task.IsCompleted) return task;
return task.ContinueWith(t => t,
default, TaskContinuationOptions.RunContinuationsAsynchronously,
TaskScheduler.Default).Unwrap();
}
public static Task<T> GetOrCreateLazyAsync<T>(this MemoryCache cache, string key,
Func<Task<T>> valueFactory, DateTimeOffset absoluteExpiration)
{
return cache.GetOrCreateLazyAsync(key, valueFactory, () => new CacheItemPolicy()
{
AbsoluteExpiration = absoluteExpiration,
});
}
public static Task<T> GetOrCreateLazyAsync<T>(this MemoryCache cache, string key,
Func<Task<T>> valueFactory, TimeSpan slidingExpiration)
{
return cache.GetOrCreateLazyAsync(key, valueFactory, () => new CacheItemPolicy()
{
SlidingExpiration = slidingExpiration,
});
}
}
该网站的HTML被下载并缓存10分钟。多个并发请求将等待同一任务完成
该类易于使用,但对缓存项排序的支持有限。基本上只有,Default
和NotRemovable
,这意味着它几乎不适合高级场景。较新的类(来自软件包)提供了有关缓存优先级的信息(Low
、Normal
、High
和NeverRemove
),但在其他方面则不那么直观,使用起来更麻烦。它提供异步功能,但不是惰性的。下面是这个类的LazyAsync等价扩展:
using Microsoft.Extensions.Caching.Memory;
static partial class MemoryCacheExtensions
{
public static Task<T> GetOrCreateLazyAsync<T>(this IMemoryCache cache, object key,
Func<Task<T>> valueFactory, MemoryCacheEntryOptions options = null)
{
if (!cache.TryGetValue(key, out Lazy<Task<T>> lazy))
{
var entry = cache.CreateEntry(key);
if (options != null) entry.SetOptions(options);
var newLazy = new Lazy<Task<T>>(valueFactory);
entry.Value = newLazy;
entry.Dispose(); // Dispose actually inserts the entry in the cache
if (!cache.TryGetValue(key, out lazy)) lazy = newLazy;
}
return ToAsyncConditional(lazy.Value);
}
private static Task<TResult> ToAsyncConditional<TResult>(Task<TResult> task)
{
if (task.IsCompleted) return task;
return task.ContinueWith(t => t,
default, TaskContinuationOptions.RunContinuationsAsynchronously,
TaskScheduler.Default).Unwrap();
}
public static Task<T> GetOrCreateLazyAsync<T>(this IMemoryCache cache, object key,
Func<Task<T>> valueFactory, DateTimeOffset absoluteExpiration)
{
return cache.GetOrCreateLazyAsync(key, valueFactory,
new MemoryCacheEntryOptions() { AbsoluteExpiration = absoluteExpiration });
}
public static Task<T> GetOrCreateLazyAsync<T>(this IMemoryCache cache, object key,
Func<Task<T>> valueFactory, TimeSpan slidingExpiration)
{
return cache.GetOrCreateLazyAsync(key, valueFactory,
new MemoryCacheEntryOptions() { SlidingExpiration = slidingExpiration });
}
}
更新:我刚刚意识到async
-await
机制的重要性。当一个不完整的任务
被同时等待多次时,continuations将一个接一个地同步运行(在同一线程中)(假设没有同步上下文)。对于上述的GetOrCreateLazyAsync
实现来说,这可能是一个问题,因为在等待调用GetOrCreateLazyAsync
之后,可能会立即阻止代码的存在,在这种情况下,其他等待程序将受到影响(延迟,甚至死锁)。此问题的一个可能解决方案是返回延迟创建的任务的异步继续,而不是任务本身,但前提是任务不完整。这就是上面引入ToAsyncConditional
方法的原因
注意:此实现缓存异步lambda调用期间可能发生的任何错误。一般来说,这可能不是理想的行为。
我可能的解决方案是将Lazy
替换为Stephen Cleary包中的类型,用RetryOnFailure
选项实例化。假设一个新线程带有相同的键,而第一个线程等待填充。相同密钥的填充器将不必要地执行两次。您可以使用
//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;
});
}
await GetOrAddAsync("foo", () => Task.Run(() => 42), i => TimeSpan.FromMilliseconds(i)));
using System.Runtime.Caching;
static partial class MemoryCacheExtensions
{
public static Task<T> GetOrCreateLazyAsync<T>(this MemoryCache cache, string key,
Func<Task<T>> valueFactory, Func<CacheItemPolicy> cacheItemPolicyFactory = null)
{
var lazyTask = (Lazy<Task<T>>)cache.Get(key);
if (lazyTask == null)
{
var newLazyTask = new Lazy<Task<T>>(valueFactory);
var cacheItem = new CacheItem(key, newLazyTask);
var cacheItemPolicy = cacheItemPolicyFactory?.Invoke();
var existingCacheItem = cache.AddOrGetExisting(cacheItem, cacheItemPolicy);
lazyTask = (Lazy<Task<T>>)existingCacheItem?.Value ?? newLazyTask;
}
return ToAsyncConditional(lazyTask.Value);
}
private static Task<TResult> ToAsyncConditional<TResult>(Task<TResult> task)
{
if (task.IsCompleted) return task;
return task.ContinueWith(t => t,
default, TaskContinuationOptions.RunContinuationsAsynchronously,
TaskScheduler.Default).Unwrap();
}
public static Task<T> GetOrCreateLazyAsync<T>(this MemoryCache cache, string key,
Func<Task<T>> valueFactory, DateTimeOffset absoluteExpiration)
{
return cache.GetOrCreateLazyAsync(key, valueFactory, () => new CacheItemPolicy()
{
AbsoluteExpiration = absoluteExpiration,
});
}
public static Task<T> GetOrCreateLazyAsync<T>(this MemoryCache cache, string key,
Func<Task<T>> valueFactory, TimeSpan slidingExpiration)
{
return cache.GetOrCreateLazyAsync(key, valueFactory, () => new CacheItemPolicy()
{
SlidingExpiration = slidingExpiration,
});
}
}
string html = await MemoryCache.Default.GetOrCreateLazyAsync("MyKey", async () =>
{
return await new WebClient().DownloadStringTaskAsync("https://stackoverflow.com");
}, DateTimeOffset.Now.AddMinutes(10));
using Microsoft.Extensions.Caching.Memory;
static partial class MemoryCacheExtensions
{
public static Task<T> GetOrCreateLazyAsync<T>(this IMemoryCache cache, object key,
Func<Task<T>> valueFactory, MemoryCacheEntryOptions options = null)
{
if (!cache.TryGetValue(key, out Lazy<Task<T>> lazy))
{
var entry = cache.CreateEntry(key);
if (options != null) entry.SetOptions(options);
var newLazy = new Lazy<Task<T>>(valueFactory);
entry.Value = newLazy;
entry.Dispose(); // Dispose actually inserts the entry in the cache
if (!cache.TryGetValue(key, out lazy)) lazy = newLazy;
}
return ToAsyncConditional(lazy.Value);
}
private static Task<TResult> ToAsyncConditional<TResult>(Task<TResult> task)
{
if (task.IsCompleted) return task;
return task.ContinueWith(t => t,
default, TaskContinuationOptions.RunContinuationsAsynchronously,
TaskScheduler.Default).Unwrap();
}
public static Task<T> GetOrCreateLazyAsync<T>(this IMemoryCache cache, object key,
Func<Task<T>> valueFactory, DateTimeOffset absoluteExpiration)
{
return cache.GetOrCreateLazyAsync(key, valueFactory,
new MemoryCacheEntryOptions() { AbsoluteExpiration = absoluteExpiration });
}
public static Task<T> GetOrCreateLazyAsync<T>(this IMemoryCache cache, object key,
Func<Task<T>> valueFactory, TimeSpan slidingExpiration)
{
return cache.GetOrCreateLazyAsync(key, valueFactory,
new MemoryCacheEntryOptions() { SlidingExpiration = slidingExpiration });
}
}
var cache = new MemoryCache(new MemoryCacheOptions());
string html = await cache.GetOrCreateLazyAsync("MyKey", async () =>
{
return await new WebClient().DownloadStringTaskAsync("https://stackoverflow.com");
}, DateTimeOffset.Now.AddMinutes(10));