ConcurrentDictionary GetOr添加异步

时间:2019-01-09 20:16:04

标签: c# .net .net-core .net-core-2.2

我想将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();} )

4 个答案:

答案 0 :(得分:2)

可能使用专用内存缓存(如 newold 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)),但我想清楚一点。