c# - ¿Por qué ConcurrentDictionary.GetOrAdd(key, valueFactory) permite que valueFactory se invoque dos veces?
multithreading caching (2)
Estoy usando un diccionario simultáneo como un caché estático seguro para subprocesos y noté el siguiente comportamiento:
Desde los documentos de MSDN en GetOrAdd :
Si llama a GetOrAdd simultáneamente en diferentes subprocesos, addValueFactory se puede llamar varias veces, pero su par de clave / valor no se puede agregar al diccionario para cada llamada.
Me gustaría poder garantizar que solo se llame a la fábrica una vez. ¿Hay alguna manera 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, así que si dos ValueFactories para la misma clave se ejecutan simultáneamente, presiono:
System.ArgumentException: Duplicate type name within an assembly.
Esto no es poco común con los algoritmos sin bloqueo . Básicamente, prueban una condición que confirma que no hay conflicto con Interlock.CompareExchange
. Sin embargo, giran alrededor hasta que el CAS tiene éxito. Eche un vistazo a ConcurrentQueue
página de ConcurrentQueue
(4) como una buena introducción a los algoritmos sin bloqueo.
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. Además de usar la otra sobrecarga de pasar un valor, necesitaría protegerse contra múltiples llamadas dentro de su fábrica de valores, tal vez usando una doble barrera de bloqueo / memoria .
Podría usar un diccionario que se escriba así: ConcurrentDictionary<TKey, Lazy<TValue>>
, y luego la fábrica de su valor devolverá un objeto Lazy<TValue>
que se ha inicializado con LazyThreadSafetyMode.ExecutionAndPublication
, que es la opción predeterminada utilizada por Lazy<TValue>
si no lo especifica. Al especificar LazyThreadSafetyMode.ExecutionAndPublication
le está diciendo a Lazy que solo un hilo puede inicializarse y establecer el valor del objeto.
Esto da como resultado que ConcurrentDictionary
solo use una instancia del objeto Lazy<TValue>
y el objeto Lazy<TValue>
proteja a más de un subproceso de la inicialización de su valor.
es decir
var dict = new ConcurrentDictionary<int, Lazy<Foo>>();
dict.GetOrAdd(key,
(k) => new Lazy<Foo>(valueFactory)
);
El inconveniente es que deberá llamar a * .Value cada vez que acceda a un objeto en el diccionario. Aquí hay algunas 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
);
}
}