这是对 Eser 的 answer(版本 2)的尝试改进。 Lazy 类默认是线程安全的,所以lock 可以被删除。可能会为给定键创建多个Lazy 对象,但只有一个对象会查询它的Value 属性,从而导致重Task 的开始。其他 Lazys 将保持未使用状态,并且将超出范围并很快被垃圾回收。
第一个重载是灵活和通用的,并接受Func<CacheItemPolicy> 参数。对于最常见的绝对到期和滑动到期情况,我添加了另外两个重载。为了方便,可以添加更多的重载。
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));
此站点的 HTML 已下载并缓存 10 分钟。多个并发请求会await完成同一个任务。
System.Runtime.Caching.MemoryCache 类易于使用,但对缓存条目优先级的支持有限。基本上只有two options、Default和NotRemovable,这意味着它对于高级场景来说是不够的。较新的Microsoft.Extensions.Caching.Memory.MemoryCache 类(来自this 包)提供了关于缓存优先级的more options(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 });
}
}
使用示例:
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));
更新:我刚刚意识到peculiar feature 的async-await 机制。当一个不完整的Task 被同时等待多次时,延续将一个接一个地同步运行(在同一个线程中)(假设没有同步上下文)。这对于GetOrCreateLazyAsync 的上述实现可能是一个问题,因为在等待调用GetOrCreateLazyAsync 之后可能会立即存在阻塞代码,在这种情况下,其他等待者将受到影响(延迟,甚至死锁)。此问题的一个可能解决方案是返回延迟创建的Task 的异步延续,而不是任务本身,但前提是任务不完整。这就是上面引入ToAsyncConditional方法的原因。
注意:此实现缓存异步 lambda 调用期间可能发生的任何错误。一般来说,这可能不是一个理想的行为。
我可能的解决方案是将 Lazy<Task<T>> 替换为 Stephen Cleary 的 Nito.AsyncEx.Coordination 包中的 AsyncLazy<T> 类型,并使用 RetryOnFailure 选项进行实例化。