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));
}