Is ConcurrentDictionary.GetOrAdd() гарантированно вызывает valueFactoryMethod только один раз за ключ?

Проблема: Мне нужно реализовать кеш объектов. Кэш должен быть потокобезопасным и должен заполнять значения по требованию (ленивая загрузка). Значения извлекаются через веб-службу с помощью клавиши (медленная работа). Поэтому я решил использовать ConcurrentDictionary и метод GetOrAdd(), который имеет метод factory, предполагающий, что операция является атомарной и синхронизированной. К сожалению, в статье MSDN я нашел следующее выражение: Как добавить и удалить элементы из ConcurrentDictionary:

Кроме того, хотя все методы ConcurrentDictionary являются поточно-безопасный, не все методы являются атомарными, в частности GetOrAdd и AddOrUpdate. Делегат пользователя, который передается этим методам, вызывается вне внутренней блокировки словаря.

Хорошо, что несчастный, но все равно не отвечает на мой ответ полностью.

Вопрос: Вызывается ли значение factory только один раз за ключ? В моем конкретном случае: возможно ли, что несколько потоков, которые ищут один и тот же ключ, порождают несколько запросов к веб-службе для одного и того же значения?

Ответ 1

Вызывается ли значение factory только один раз за ключ?

Нет, это не так. Документы говорят:

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

Ответ 2

Как уже указывали другие, valueFactory может вызываться более одного раза. Существует общее решение, которое смягчает эту проблему: верните valueFactory экземпляр Lazy<T>. Хотя возможно создание нескольких ленивых экземпляров, фактическое значение T будет создано только при доступе к свойству Lazy<T>.Value.

В частности:

// Lazy instance may be created multiple times, but only one will actually be used.
// GetObjectFromRemoteServer will not be called here.
var lazyObject = dict.GetOrAdd("key", key => new Lazy<MyObject>(() => GetObjectFromRemoteServer()));

// Only here GetObjectFromRemoteServer() will be called.
// The next calls will not go to the server
var myObject = lazyObject.Value;

Этот метод далее объясняется в Сообщение блога Рида Копси

Ответ 3

Посмотрим на исходный код GetOrAdd:

public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
{
    if (key == null) throw new ArgumentNullException("key");
    if (valueFactory == null) throw new ArgumentNullException("valueFactory");

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

К сожалению, в этом случае он ничего не гарантирует, что valueFactory не будет вызываться более одного раза, если две вызовы GetOrAdd выполняются параллельно.