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

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

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

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

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

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

System.ArgumentException: Duplicate type name within an assembly.


person ChaseMedallion    schedule 26.09.2012    source источник


Ответы (2)


Вы можете использовать словарь, который имеет следующий тип: ConcurrentDictionary<TKey, Lazy<TValue>>, и тогда ваша фабрика значений вернет объект Lazy<TValue>, который был инициализирован с помощью LazyThreadSafetyMode.ExecutionAndPublication, который является параметром по умолчанию, используемым Lazy<TValue>, если вы его не укажете. Указав LazyThreadSafetyMode.ExecutionAndPublication, вы сообщаете Lazy, что только один поток может инициализировать и устанавливать значение объекта.

Это приводит к тому, что ConcurrentDictionary использует только один экземпляр объекта Lazy<TValue>, а объект Lazy<TValue> защищает более одного потока от инициализации своего значения.

i.e.

var dict = new ConcurrentDictionary<int, Lazy<Foo>>();
dict.GetOrAdd(key,  
    (k) => new Lazy<Foo>(valueFactory)
);

Недостатком является то, что вам нужно будет вызывать *.Value каждый раз, когда вы обращаетесь к объекту в словаре. Вот некоторые расширения, которые помогут в этом. .

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 ([email protected](key, out Lazy<TValue> existingValue))
           return false;

       return @this.TryUpdate(key,
           new Lazy<TValue>(
               () => updateValueFactory(key, existingValue.Value)
           ),
           existingValue
       );
   }
}
person JG in SD    schedule 26.09.2012
comment
LazyThreadSafetyMode.ExecutionAndPublication используется по умолчанию и может быть опущен. - person yaakov; 28.02.2017
comment
@yaakov правда. Для меня значение по умолчанию всегда было неясным, поэтому я предпочитаю всегда указывать его. - person usr; 18.03.2018
comment
Учитывая, сколько методов вы расширяете, вероятно, имеет смысл просто написать класс ConcurrentLazyDictionary, который прозрачно инкапсулирует использование Lazy. - person Ian Kemp; 16.04.2018

Это не редкость для неблокирующих алгоритмов. По сути, они проверяют условие, подтверждающее отсутствие разногласий, используя Interlock.CompareExchange. Однако они зацикливаются до тех пор, пока CAS не добьется успеха. Взгляните на страницу ConcurrentQueue (4) в качестве хорошее введение в неблокирующие алгоритмы

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

person M Afifi    schedule 26.09.2012