C# 缓存异步操作

C# 缓存异步操作,c#,.net,multithreading,async-await,concurrentdictionary,C#,.net,Multithreading,Async Await,Concurrentdictionary,我正在寻找一种优雅的方式来缓存异步操作的结果 我首先有一个同步方法,如下所示: public String GetStuff(String url) { WebRequest request = WebRequest.Create(url); using (var response = request.GetResponse()) using (var sr = new StreamReader(response.GetResponseStream()))

我正在寻找一种优雅的方式来缓存异步操作的结果

我首先有一个同步方法,如下所示:

public String GetStuff(String url)
{
    WebRequest request = WebRequest.Create(url);
    using (var response = request.GetResponse())
    using (var sr = new StreamReader(response.GetResponseStream()))
        return sr.ReadToEnd();
}
然后我让它异步:

public async Task<String> GetStuffAsync(String url)
{
    WebRequest request = WebRequest.Create(url);
    using (var response = await request.GetResponseAsync())
    using (var sr = new StreamReader(response.GetResponseStream()))
        return await sr.ReadToEndAsync();
}
公共异步任务GetStuffAsync(字符串url) { WebRequest=WebRequest.Create(url); 使用(var response=wait request.GetResponseAsync()) 使用(var sr=newstreamreader(response.GetResponseStream()) return wait sr.ReadToEndAsync(); } 然后我决定应该缓存结果,因此我不需要经常在外部查询:

ConcurrentDictionary<String, String> _cache = new ConcurrentDictionary<String, String>();

public async Task<String> GetStuffAsync(String url)
{
    return _cache.GetOrAdd(url, await GetStuffInternalAsync(url));
}

private async Task<String> GetStuffInternalAsync(String url)
{
    WebRequest request = WebRequest.Create(url);
    using (var response = await request.GetResponseAsync())
    using (var sr = new StreamReader(response.GetResponseStream()))
        return await sr.ReadToEndAsync();
}
ConcurrentDictionary\u cache=new ConcurrentDictionary();
公共异步任务GetStuffAsync(字符串url)
{
返回_cache.GetOrAdd(url,等待GetStuffInternalAsync(url));
}
专用异步任务GetStuffInternalAsync(字符串url)
{
WebRequest=WebRequest.Create(url);
使用(var response=wait request.GetResponseAsync())
使用(var sr=newstreamreader(response.GetResponseStream())
return wait sr.ReadToEndAsync();
}
然后我读了一篇文章(观看了一段视频),内容是关于缓存
任务
如何更好,因为创建它们的成本很高:

ConcurrentDictionary<String, Task<String>> _cache = new ConcurrentDictionary<String, Task<String>>();

public Task<String> GetStuffAsync(String url)
{
    return _cache.GetOrAdd(url, GetStuffInternalAsync(url));
}

private async Task<String> GetStuffInternalAsync(String url)
{
    WebRequest request = WebRequest.Create(url);
    using (var response = await request.GetResponseAsync())
    using (var sr = new StreamReader(response.GetResponseStream()))
        return await sr.ReadToEndAsync();
}
ConcurrentDictionary\u cache=new ConcurrentDictionary();
公共任务GetStuffAsync(字符串url)
{
返回_cache.GetOrAdd(url,GetStuffInternalAsync(url));
}
专用异步任务GetStuffInternalAsync(字符串url)
{
WebRequest=WebRequest.Create(url);
使用(var response=wait request.GetResponseAsync())
使用(var sr=newstreamreader(response.GetResponseStream())
return wait sr.ReadToEndAsync();
}
现在的问题是,如果请求失败(例如HTTP 401),缓存将包含失败的
任务
,我将不得不重置应用程序,因为无法重新发送请求


有没有一种优雅的方法可以使用
ConcurrentDictionary
只缓存成功的任务,并且仍然具有原子行为?

首先,您的两种方法都是错误的,因为它们不会为您节省任何请求(尽管第二种方法至少可以为您节省时间)

您的第一个代码(带有
wait
的代码)执行以下操作:

  • 提出请求
  • 等待请求完成
  • 如果缓存中已有结果,请忽略请求的结果
  • 您的第二个代码删除了步骤2,因此速度更快,但您仍在发出大量不必要的请求

    您应该做的是使用:

    将这些失败的任务作为一个整体保留实际上是合理的(并且取决于您的设计和性能,这一点至关重要)。否则,如果
    url
    总是失败,那么反复使用它就无法完全使用缓存

    您需要的是一种随时清除缓存的方法。最简单的方法是使用一个计时器来替换
    ConcurrentDictionary
    实例。更可靠的解决方案是构建您自己的
    lAudictionary
    或类似的东西。

    这项工作对我来说:

    ObjectCache _cache = MemoryCache.Default;
    static object _lockObject = new object();
    public Task<T> GetAsync<T>(string cacheKey, Func<Task<T>> func, TimeSpan? cacheExpiration = null) where T : class
    {
        var task = (T)_cache[cacheKey];
        if (task != null) return task;          
        lock (_lockObject)
        {
            task = (T)_cache[cacheKey](cacheKey);
            if (task != null) return task;
            task = func();
            Set(cacheKey, task, cacheExpiration);
            task.ContinueWith(t => {
                if (t.Status != TaskStatus.RanToCompletion)
                    _cache.Remove(cacheKey);
            });
        }
        return task;
    } 
    
    ObjectCache\u cache=MemoryCache.Default;
    静态对象_lockObject=新对象();
    公共任务GetAsync(字符串cacheKey,Func Func,TimeSpan?cacheExpiration=null),其中T:class
    {
    var task=(T)u cache[cacheKey];
    如果(task!=null)返回任务;
    锁定(锁定对象)
    {
    任务=(T)u缓存[cacheKey](cacheKey);
    如果(task!=null)返回任务;
    task=func();
    设置(cacheKey、task、cacheExpiration);
    task.ContinueWith(t=>{
    if(t.Status!=TaskStatus.RanToCompletion)
    _cache.Remove(cacheKey);
    });
    }
    返回任务;
    } 
    
    这里有一种缓存异步操作结果的方法,可以保证不会出现缓存未命中

    在接受的答案中,如果在循环中多次请求相同的url(取决于SynchronizationContext)或从多个线程请求相同的url,则web请求将一直被发送,直到有响应被缓存,此时缓存将开始被使用

    下面的方法为每个唯一键创建一个对象。这将防止长时间运行的异步操作为同一个密钥运行多次,同时允许它为不同的密钥同时运行。显然,保留SemaphoreSlim对象是有开销的,以防止缓存未命中,因此根据使用情况,可能不值得这样做。但是,如果保证没有缓存未命中是重要的,那么这就实现了这一点

    private readonly ConcurrentDictionary<string, SemaphoreSlim> _keyLocks = new ConcurrentDictionary<string, SemaphoreSlim>();
    private readonly ConcurrentDictionary<string, string> _cache = new ConcurrentDictionary<string, string>();
    
    public async Task<string> GetSomethingAsync(string key)
    {   
        string value;
        // get the semaphore specific to this key
        var keyLock = _keyLocks.GetOrAdd(key, x => new SemaphoreSlim(1));
        await keyLock.WaitAsync();
        try
        {
            // try to get value from cache
            if (!_cache.TryGetValue(key, out value))
            {
                // if value isn't cached, get it the long way asynchronously
                value = await GetSomethingTheLongWayAsync();
    
                // cache value
                _cache.TryAdd(key, value);
            }
        }
        finally
        {
            keyLock.Release();
        }
        return value;
    }
    
    专用只读ConcurrentDictionary _keyLocks=new ConcurrentDictionary();
    私有只读ConcurrentDictionary _cache=新ConcurrentDictionary();
    公共异步任务GetSomethingAsync(字符串键)
    {   
    字符串值;
    //获取特定于此键的信号量
    var keyLock=_keyLocks.GetOrAdd(key,x=>newsemaphoreslim(1));
    wait keyLock.WaitAsync();
    尝试
    {
    //尝试从缓存中获取值
    if(!\u cache.TryGetValue(键,输出值))
    {
    //如果未缓存该值,则长时间异步获取该值
    value=等待GetSomethingTheLongWayAsync();
    //缓存值
    _cache.TryAdd(键,值);
    }
    }
    最后
    {
    钥匙锁。释放();
    }
    返回值;
    }
    

    编辑:正如@mtkachenko在评论中提到的,可以在该方法开始时执行额外的缓存检查,以潜在地跳过锁获取步骤。

    另一种简单的方法是将
    惰性
    扩展为
    异步惰性
    ,如下所示:

    public class AsyncLazy<T> : Lazy<Task<T>>
    {
        public AsyncLazy(Func<Task<T>> taskFactory, LazyThreadSafetyMode mode) :
            base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap(), mode)
        { }
    
        public TaskAwaiter<T> GetAwaiter() { return Value.GetAwaiter(); }
    }
    
    公共类异步延迟:延迟
    {
    公共异步模式(Func taskFactory,LazyThreadSafetyMode模式):
    基本(()=>Task.Factory.StartNew(()=>taskFactory()).Unwrap(),模式)
    { }
    公共任务等待器GetAwaiter(){return Value.GetAwaiter();}
    }
    
    然后你可以这样做:

    private readonly ConcurrentDictionary<string, AsyncLazy<string>> _cache
        = new ConcurrentDictionary<string, AsyncLazy<string>>();
    
    public async Task<string> GetStuffAsync(string url)
    {
        return await _cache.GetOrAdd(url,
            new AsyncLazy<string>(
                () => GetStuffInternalAsync(url),
                LazyThreadSafetyMode.ExecutionAndPublication));
    }
    
    专用只读ConcurrentDictionary\u缓存
    =新的ConcurrentDictionary();
    公共异步任务GetStuffAsync(字符串url)
    {
    
    public class AsyncLazy<T> : Lazy<Task<T>>
    {
        public AsyncLazy(Func<Task<T>> taskFactory, LazyThreadSafetyMode mode) :
            base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap(), mode)
        { }
    
        public TaskAwaiter<T> GetAwaiter() { return Value.GetAwaiter(); }
    }
    
    private readonly ConcurrentDictionary<string, AsyncLazy<string>> _cache
        = new ConcurrentDictionary<string, AsyncLazy<string>>();
    
    public async Task<string> GetStuffAsync(string url)
    {
        return await _cache.GetOrAdd(url,
            new AsyncLazy<string>(
                () => GetStuffInternalAsync(url),
                LazyThreadSafetyMode.ExecutionAndPublication));
    }