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
выполняются параллельно.