C# 异步线程安全从MemoryCache获取

C# 异步线程安全从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);

我创建了一个异步缓存,它在下面使用.NET
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));