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

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

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

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

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

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

System.ArgumentException: дублировать имя типа в сборке.

Ответ 1

Вы можете использовать словарь, который вводится следующим образом: ConcurrentDictionary<TKey, Lazy<TValue>>, а затем ваше значение factory вернет объект 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)
);

Тогда вам нужно будет вызывать *.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
       );
   }
}

Ответ 2

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

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