我想将GetOrAdd
之类的ConcurrentDictionary
用作Web服务的缓存。该词典有异步版本吗? GetOrAdd将使用HttpClient
发出Web请求,因此,如果该词典的某个版本中GetOrAdd是异步的,那就太好了。
为消除混乱,字典的内容将是对Web服务的调用的响应。
ConcurrentDictionary<string, Response> _cache = new ConcurrentDictionary<string, Response>();
var response = _cache.GetOrAdd("id", (x) => { _httpClient.GetAsync(x).GetAwaiter().GetResponse();} )
答案 0 :(得分:2)
可能使用专用内存缓存(如 new 或 old MemoryCache
类,或 this 第三方库)应该比使用简单的 { {3}}。除非你真的不需要常用的功能,比如基于时间的过期、基于大小的压缩、自动驱逐依赖于其他已过期条目或依赖于可变外部资源(如文件、数据库等)的条目。不过应该注意的是,ConcurrentDictionary
可能仍然需要一些工作才能正确处理异步委托,因为它的开箱即用行为 MemoryCache
。
以下是具有 GetOrAddAsync
值的 ConcurrentDictionary
的自定义扩展方法 Task<TValue>
。它接受一个工厂方法,并确保该方法最多被调用一次。它还确保从字典中删除失败的任务。此实现针对频繁获取现有任务而很少创建新任务的情况进行了优化。
/// <summary>
/// Returns an existing task from the concurrent dictionary, or adds a new task
/// using the specified asynchronous factory method. Concurrent invocations for
/// the same key are prevented, unless the task is removed before the completion
/// of the delegate. Failed tasks are evicted from the concurrent dictionary.
/// </summary>
public static Task<TValue> GetOrAddAsync<TKey, TValue>(
this ConcurrentDictionary<TKey, Task<TValue>> source, TKey key,
Func<TKey, Task<TValue>> valueFactory)
{
if (!source.TryGetValue(key, out var currentTask))
{
Task<TValue> newTask = null;
var newTaskTask = new Task<Task<TValue>>(async () =>
{
try { return await valueFactory(key).ConfigureAwait(false); }
catch
{
((ICollection<KeyValuePair<TKey, Task<TValue>>>)source)
.Remove(new KeyValuePair<TKey, Task<TValue>>(key, newTask));
//source.TryRemove(KeyValuePair.Create(key, newTask)); // .NET 5
throw;
}
});
newTask = newTaskTask.Unwrap();
currentTask = source.GetOrAdd(key, newTask);
if (currentTask == newTask)
newTaskTask.RunSynchronously(TaskScheduler.Default);
}
return currentTask;
}
用法示例:
var cache = new ConcurrentDictionary<string, Task<HttpResponseMessage>>();
var response = await cache.GetOrAddAsync("https://stackoverflow.com", async url =>
{
return await _httpClient.GetAsync(url);
});
为了移除故障任务,此实现使用了显式实现的 is not ideal API。有关此 API 的更多信息,请参见 ICollection<T>.Remove
。从 .NET 5 开始,可以改用新的 here 方法。
顺便说一句,如果需要最高性能,您可能需要查看 TryRemove(KeyValuePair<TKey, TValue> item)
第三方库。我从未亲自使用过它,但它的基准图表看起来令人印象深刻。
答案 1 :(得分:1)
GetOrAdd
方法不适用于此目的。由于不能保证工厂只能运行一次,因此它的唯一目的是进行次要优化(因为很少会进行添加,因此是次要的优化),因为它不需要哈希并两次查找正确的存储区(如果您会收到并设置两个单独的电话)。
我建议您先检查缓存,如果您在缓存中找不到该值,然后输入某种形式的关键部分(锁定,信号量等),重新检查缓存,如果仍然丢失,则取值并插入到缓存中。
这可确保您的后备店仅被击中一次;即使多个请求同时遇到缓存未命中,只有第一个请求实际上会获取值,其他请求将等待信号量,然后由于它们在关键部分重新检查了缓存而提早返回。
伪代码(使用SemaphoreSlim的计数为1,因为您可以异步等待它):
async Task<TResult> GetAsync(TKey key)
{
// Try to fetch from catch
if (cache.TryGetValue(key, out var result)) return result;
// Get some resource lock here, for example use SemaphoreSlim
// which has async wait function:
await semaphore.WaitAsync();
try
{
// Try to fetch from cache again now that we have entered
// the critical section
if (cache.TryGetValue(key, out result)) return result;
// Fetch data from source (using your HttpClient or whatever),
// update your cache and return.
return cache[key] = await FetchFromSourceAsync(...);
}
finally
{
semaphore.Release();
}
}
答案 2 :(得分:1)
GetOrAdd
不会成为异步操作,因为访问字典的值并不是一项长期运行的操作。
但是,您可以做的只是将任务存储在字典中,而不是具体化的结果。任何需要结果的人都可以等待该任务。
但是,您还需要确保该操作仅启动一次,而不是多次启动。为确保某些操作仅运行一次,而不是多次运行,您还需要添加Lazy
:
ConcurrentDictionary<string, Lazy<Task<Response>>> _cache = new ConcurrentDictionary<string, Lazy<Task<Response>>>();
var response = await _cache.GetOrAdd("id", url => new Lazy<Response>(_httpClient.GetAsync(x))).Value;
答案 3 :(得分:0)
尝试此扩展方法:
git merge
使用/// <summary>
/// Adds a key/value pair to the <see cref="ConcurrentDictionary{TKey, TValue}"/> by using the specified function
/// if the key does not already exist. Returns the new value, or the existing value if the key exists.
/// </summary>
public static async Task<TResult> GetOrAddAsync<TKey,TResult>(
this ConcurrentDictionary<TKey,TResult> dict,
TKey key, Func<TKey,Task<TResult>> asyncValueFactory)
{
if (dict.TryGetValue(key, out TResult resultingValue))
{
return resultingValue;
}
var newValue = await asyncValueFactory(key);
return dict.GetOrAdd(key, newValue);
}
代替dict.GetOrAdd(key,key=>something(key))
。显然,在这种情况下,您只需将其写为await dict.GetOrAddAsync(key,async key=>await something(key))
,但我想清楚一点。