Как сохранить результат асинхронного метода в .NET ConcurrentDictionary при вызове GetOrAdd?

У меня есть private ConcurrentDictionary, это простая таблица поиска некоторых ключей БД.

Я пытаюсь использовать ConcurrentDictionary, чтобы он выполнял только один вызов БД, когда одновременно выполняются 2+ запроса на одну и ту же строку кода. (Вот почему я использую ConcurrentDictionary.)

Как я могу это сделать, пожалуйста?

Это то, что я пытался сделать... но я думаю, что это сохранение Task в словаре... а не результат задачи....

private readonly ConcurrentDictionary<string, Task<int>> _myKeys = new ConcurrentDictionary<string, Task<int>>();

...

private async Task<int> DoStuffAsync(string key)
{
   // do stuff here.

   return await _myKeys.GetOrAdd(key,
                                 async k => await _db.GetId(k)
                                                     .ConfigureAwait(false))
                       .ConfigureAwait(false);
}

Любые идеи?

РЕДАКТИРОВАТЬ:

Обратите внимание на сигнатуру моего метода и на то, что я возвращаю. Лучше ли вернуть int, а не Task<int>, а затем каким-то образом реорганизовать мой вызов БД, чтобы он все еще был асинхронным... но... лучше?


person Pure.Krome    schedule 17.10.2016    source источник
comment
Что не так с сохранением задачи в словаре?   -  person Stephen Cleary    schedule 17.10.2016
comment
Люди, которые голосуют за закрытие или голосование против, пожалуйста, объясните, почему, чтобы улучшить Q.   -  person Pure.Krome    schedule 18.10.2016
comment
@StephenCleary Я не был уверен, хорошо это или плохо. Это было похоже на тяжелый объект для хранения, когда результатом будет просто int.   -  person Pure.Krome    schedule 18.10.2016


Ответы (1)


GetOrAdd не гарантирует, что делегат будет вызван только один раз. когда он вызывается из нескольких потоков одновременно с одним и тем же значением:

Если вы вызываете GetOrAdd одновременно в разных потоках, addValueFactory может вызываться несколько раз, но его пара ключ/значение может не добавляться в словарь при каждом вызове.

Это также можно увидеть в реализации:

TValue resultingValue;
if (TryGetValue(key, out resultingValue))
{
    return resultingValue;
}
TryAddInternal(key, valueFactory(key), false, true, out resultingValue);
return resultingValue;

Итак, чтобы сделать работу примерно такой же хорошей, как GetOrAdd(), вы можете сделать что-то вроде (проверка ввода опущена):

public static async Task<TValue> GetOrAddAsync<TKey, TValue>(
    this ConcurrentDictionary<TKey, TValue> dictionary,
    TKey key, Func<TKey, Task<TValue>> valueFactory)
{
    TValue resultingValue;
    if (dictionary.TryGetValue(key, out resultingValue))
    {
        return resultingValue;
    }
    return dictionary.GetOrAdd(key, await valueFactory(key));
}

Если требование не вызывать делегата дважды одновременно является просто оптимизацией производительности, этого должно быть достаточно.

Если это требуется для корректности вашего кода, то даже GetOrAdd недостаточно и вам потребуется использовать дополнительную синхронизацию.

person svick    schedule 17.10.2016
comment
Я подозреваю, что это хуже, чем код OP для типичных случаев: в вашей версии предположим, что задача занимает пять секунд, а через три секунды другая задача вызывает GetOrAddAsync для того же ключа. В вашем коде это гарантированно запускает новую задачу. В коде OP очень вероятно повторное использование существующей задачи. Да, вы правы в том, что код OP ничего не гарантирует, но это делает гораздо более вероятным, что я бы выбрал версию OP. - person ; 17.10.2016
comment
@hvd Ты прав. Я работал в предположении, что вы не хотите хранить Task в словаре. - person svick; 17.10.2016