2012-09-26 12 views
26

Używam współbieżne słownika jako bezpieczny wątku statycznej pamięci podręcznej i zauważyłem następujący problem:Dlaczego ConcurrentDictionary.GetOrAdd (key, valueFactory) pozwala na wywołanie właściwości valueFactory dwa razy?

Od the MSDN docs on GetOrAdd:

Jeśli zadzwonisz GetOrAdd równocześnie na różnych wątkach, addValueFactory może zostać wywołana wielokrotnie , ale jego para klucz/wartość może nie zostać dodana do słownika dla każdego połączenia.

Chciałbym być w stanie zagwarantować, że fabryka jest wywoływana tylko raz. Czy jest jakiś sposób, aby to zrobić z API ConcurrentDictionary bez uciekania się do mojej własnej oddzielnej synchronizacji (np. Blokowanie wewnątrz valueFactory)?

Moje przypadek użycia jest to, że valueFactory generuje typy wewnątrz modułu dynamicznego więc jeśli dwie valueFactories na tym samym biegu kluczowego jednocześnie uderzę:

System.ArgumentException: zduplikowane nazwy typu w zespole.

Odpowiedz

35

Można używać słownika, który jest wpisany w ten sposób: ConcurrentDictionary<TKey, Lazy<TValue>>, a następnie swoją fabrycznie wartość zwróci Lazy<TValue> obiekt, który został zainicjowany z LazyThreadSafetyMode.ExecutionAndPublication, która to opcja domyślna używana przez Lazy<TValue> jeśli nie podasz go . Podając LazyThreadSafetyMode.ExecutionAndPublication mówisz Lazy tylko jeden wątek może zainicjować i ustawić wartość obiektu.

Powoduje to, że ConcurrentDictionary używa tylko jednej instancji obiektu Lazy<TValue>, a obiekt Lazy<TValue> chroni więcej niż jeden wątek przed zainicjowaniem jego wartości.

tj

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

Minusem jest więc musisz zadzwonić * .Value każdym razem uzyskuje dostęp do obiektu w słowniku. Oto niektóre extensions, które pomogą w tym.

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 
     ); 
    } 
} 
+0

'LazyThreadSafetyMode.ExecutionAndPublication' jest wartością domyślną i można ją pominąć. – yaakov

5

Nie jest to rzadkością z Non-Blocking Algorithms. Zasadniczo testują warunek potwierdzający, że nie ma żadnych niezgodności z użyciem Interlock.CompareExchange. Obracają się jednak, dopóki CAS się nie powiedzie. Spójrz na stronę ConcurrentQueue (4) jako dobre intro do Non-Blocking Algorithms

Krótka odpowiedź brzmi: nie, to natura bestii będzie wymagała wielu prób dodania do kolekcji w ramach rywalizacji. Oprócz użycia innego przeciążenia podania wartości, musisz chronić się przed wieloma połączeniami w fabryce wartości, być może używając double lock/memory barrier.