Почему делает ConcurrentDictionary.GetOrAdd (ключ, valueFactory) позволяет вызывать valueFactory дважды?


Я использую параллельный словарь в качестве потокобезопасного статического кэша и заметил следующее поведение:

Из документов MSDN на GetOrAdd :

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

Я хотел бы быть в состоянии гарантировать, что завод будет вызван только один раз. Есть ли способ сделать это с помощью ConcurrentDictionary API без использования моей собственной отдельной синхронизации (например, блокировка внутри valueFactory)?

Мой пример использования заключается в том, что valueFactory генерирует типы внутри динамического модуля, поэтому, если два valueFactories для одного и того же ключа выполняются одновременно, я нажимаю:

System.ArgumentException: Duplicate type name within an assembly.

2 26

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) как хорошее вступление к неблокирующим алгоритмам

Короткий ответ-Нет, это Природа зверя, что он потребует многократных попыток добавить в коллекцию под раздором. Кроме использования другой перегрузки передачи значения, вам нужно будет защитить себя от нескольких вызовов внутри вашей фабрики значений, возможно, используя двойной барьер блокировки / памяти .