2012-09-26 20 views
26

estoy usando un diccionario concurrente como una caché estática flujos seguros y notamos el siguiente comportamiento:¿Por qué ConcurrentDictionary.GetOrAdd (key, valueFactory) permite que valueFactory se invoque dos veces?

De the MSDN docs on GetOrAdd:

Si llama GetOrAdd simultáneamente en diferentes hilos, addValueFactory se puede llamar varias veces , pero su par clave/valor podría no agregarse al diccionario para cada llamada.

Me gustaría poder garantizar que la fábrica solo se llame una vez. ¿Hay alguna forma de hacer esto con la API ConcurrentDictionary sin recurrir a mi propia sincronización por separado (por ejemplo, el bloqueo dentro de valueFactory)?

Mi caso de uso es que valueFactory está generando tipos dentro de un módulo dinámico de modo que si dos valueFactories para la misma clave se ejecutan al mismo tiempo me golpeó:

System.ArgumentException: Duplicar nombre de tipo dentro de un conjunto.

Respuesta

35

Se puede usar un diccionario que se escribe así: ConcurrentDictionary<TKey, Lazy<TValue>>, y después la fábrica de su valor sería devolver un objeto Lazy<TValue> que se ha inicializado con LazyThreadSafetyMode.ExecutionAndPublication, que es la opción por defecto utilizado por Lazy<TValue> si no lo especifica . Al especificar el LazyThreadSafetyMode.ExecutionAndPublication le está diciendo a Lazy que solo un hilo puede inicializarse y establecer el valor del objeto.

Esto da como resultado el ConcurrentDictionary solo utilizando una instancia del objeto Lazy<TValue>, y el objeto Lazy<TValue> protege más de un subproceso de la inicialización de su valor.

decir

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

La desventaja es, entonces, tendrá que llamar * .Value cada vez que accede a un objeto en el diccionario. Aquí hay algunos extensions que te ayudarán con eso.

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' es el valor predeterminado y se puede omitir. – yaakov

5

Esto no es raro con Non-Blocking Algorithms. Básicamente, prueban una condición que confirma que no hay contención usando Interlock.CompareExchange. Sin embargo, giran alrededor hasta que el CAS tiene éxito. Eche un vistazo a ConcurrentQueue página (4) como una buena introducción a Non-Blocking Algorithms

La respuesta corta es no, es la naturaleza de la bestia que requerirá múltiples intentos de agregar a la colección bajo contención. Aparte de usar la otra sobrecarga de pasar un valor, necesitaría protegerse contra múltiples llamadas dentro de su fábrica de valores, quizás usando un double lock/memory barrier.

Cuestiones relacionadas