Почему делает ConcurrentDictionary.GetOrAdd (ключ, valueFactory) позволяет вызывать valueFactory дважды?
Я использую параллельный словарь в качестве потокобезопасного статического кэша и заметил следующее поведение:
Из документов MSDN на GetOrAdd :
Если вы вызываете GetOrAdd одновременно в разных потоках, addValueFactory может вызываться несколько раз, но его пара ключ / значение возможно, не будет добавляться в словарь для каждого вызова.
Я хотел бы быть в состоянии гарантировать, что завод будет вызван только один раз. Есть ли способ сделать это с помощью ConcurrentDictionary API без использования моей собственной отдельной синхронизации (например, блокировка внутри valueFactory)?
Мой пример использования заключается в том, что valueFactory генерирует типы внутри динамического модуля, поэтому, если два valueFactories для одного и того же ключа выполняются одновременно, я нажимаю:
System.ArgumentException: Duplicate type name within an assembly.
2 ответа:
Можно использовать словарь, типизированный следующим образом:
ConcurrentDictionary<TKey, Lazy<TValue>>
, и тогда фабрика значений возвращает объектLazy<TValue>
, который был инициализирован с помощьюLazyThreadSafetyMode.ExecutionAndPublication
, что является параметром по умолчанию, используемымLazy<TValue>
, Если вы его не указываете. УказываяLazyThreadSafetyMode.ExecutionAndPublication
, Вы говорите Lazy, что только один поток может инициализировать и установить значение объекта.Это приводит к тому, что
ConcurrentDictionary
использует только один экземпляр объектаLazy<TValue>
, а объектLazy<TValue>
защищает более чем один поток от инициализации его значение.То есть
var dict = new ConcurrentDictionary<int, Lazy<Foo>>(); dict.GetOrAdd(key, (k) => new Lazy<Foo>(valueFactory) );
Недостатком является то, что вам нужно будет позвонить *.Значение каждый раз, когда вы обращаетесь к объекту в словаре. Вот некоторые расширения , которые помогут с этим.
public static class ConcurrentDictionaryExtensions { public static TValue GetOrAdd<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> @this, TKey key, Func<TKey, TValue> valueFactory ) { return @this.GetOrAdd(key, (k) => new Lazy<TValue>(() => valueFactory(k)) ).Value; } public static TValue AddOrUpdate<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> @this, TKey key, Func<TKey, TValue> addValueFactory, Func<TKey, TValue, TValue> updateValueFactory ) { return @this.AddOrUpdate(key, (k) => new Lazy<TValue>(() => addValueFactory(k)), (k, currentValue) => new Lazy<TValue>( () => updateValueFactory(k, currentValue.Value) ) ).Value; } public static bool TryGetValue<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> @this, TKey key, out TValue value ) { value = default(TValue); var result = @this.TryGetValue(key, out Lazy<TValue> v); if (result) value = v.Value; return result; } // this overload may not make sense to use when you want to avoid // the construction of the value when it isn't needed public static bool TryAdd<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> @this, TKey key, TValue value ) { return @this.TryAdd(key, new Lazy<TValue>(() => value)); } public static bool TryAdd<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> @this, TKey key, Func<TKey, TValue> valueFactory ) { return @this.TryAdd(key, new Lazy<TValue>(() => valueFactory(key)) ); } public static bool TryRemove<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> @this, TKey key, out TValue value ) { value = default(TValue); if (@this.TryRemove(key, out Lazy<TValue> v)) { value = v.Value; return true; } return false; } public static bool TryUpdate<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> @this, TKey key, Func<TKey, TValue, TValue> updateValueFactory ) { if (!@this.TryGetValue(key, out Lazy<TValue> existingValue)) return false; return @this.TryUpdate(key, new Lazy<TValue>( () => updateValueFactory(key, existingValue.Value) ), existingValue ); } }
Это не редкость для неблокирующих алгоритмов. Они по существу проверяют условие, подтверждающее отсутствие конкуренции, используя
Interlock.CompareExchange
. Они петляют вокруг, пока CAS не добьется успеха. Взгляните наConcurrentQueue
Страница (4) как хорошее вступление к неблокирующим алгоритмамКороткий ответ-Нет, это Природа зверя, что он потребует многократных попыток добавить в коллекцию под раздором. Кроме использования другой перегрузки передачи значения, вам нужно будет защитить себя от нескольких вызовов внутри вашей фабрики значений, возможно, используя двойной барьер блокировки / памяти .