2012-09-26 9 views
26

sto usando un dizionario concomitante come una cache statica thread-safe e ho notato il seguente comportamento:Perché ConcurrentDictionary.GetOrAdd (key, valueFactory) consente a valueFactory di essere invocato due volte?

Da the MSDN docs on GetOrAdd:

Se si chiama GetOrAdd contemporaneamente su diversi thread, addValueFactory può essere chiamato più volte , ma la coppia chiave/valore potrebbe non essere aggiunta al dizionario per ogni chiamata.

Mi piacerebbe essere in grado di garantire che la fabbrica venga chiamata una sola volta. C'è un modo per farlo con l'API ConcurrentDictionary senza ricorrere alla mia sincronizzazione separata (ad esempio bloccando dentro valueFactory)?

mio caso d'uso è che valueFactory sta generando tipi all'interno di un modulo dinamico quindi se due valueFactories per lo stesso periodo chiave contemporaneamente I hit:

System.ArgumentException: duplicato nome tipo all'interno di un assieme.

risposta

35

Si potrebbe usare un dizionario che viene digitato in questo modo: ConcurrentDictionary<TKey, Lazy<TValue>>, e poi la vostra fabbrica valore sarebbe restituire un oggetto Lazy<TValue> che è stato inizializzato con LazyThreadSafetyMode.ExecutionAndPublication, che è l'opzione predefinita utilizzata da Lazy<TValue> se non si specifica che . Specificando LazyThreadSafetyMode.ExecutionAndPublication che stai dicendo a Lazy, solo un thread può inizializzarsi e impostare il valore dell'oggetto.

Ciò comporta che l'ConcurrentDictionary utilizza solo un'istanza dell'oggetto Lazy<TValue> e che l'oggetto Lazy<TValue> protegge più di un thread dall'inizializzazione del suo valore.

cioè

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

Il rovescio della medaglia è quindi avrete bisogno di chiamare * .Value ogni volta che si accede a un oggetto nel dizionario. Ecco alcuni extensions che ti aiuteranno in questo.

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' è l'impostazione predefinita e può essere ommited. – yaakov

5

Questo non è raro con Non-Blocking Algorithms. In sostanza testano una condizione in cui si confermi che non vi è alcuna contesa usando Interlock.CompareExchange. Si aggirano però fino a quando il CAS ha successo. Date un'occhiata alla pagina ConcurrentQueue (4) come una buona intro a Non-Blocking Algorithms

La risposta breve è no, è la natura della bestia che richiederà più tentativi di aggiungere alla collezione in lizza. Oltre a utilizzare l'altro sovraccarico per il passaggio di un valore, è necessario proteggersi da più chiamate all'interno del proprio valore, magari utilizzando uno double lock/memory barrier.

Problemi correlati