c# - MemoryCache Thread Safety, ¿es necesario bloquear?
multithreading wcf (5)
Para empezar, permítanme decir que sé que el código siguiente no es seguro para subprocesos (corrección: podría ser). Lo que estoy luchando es encontrar una implementación que sea y que realmente pueda llegar a fallar bajo prueba. Estoy refabricando un gran proyecto de WCF en este momento que necesita algunos (la mayoría) de los datos estáticos en caché y se llena desde una base de datos SQL. Debe caducar y "actualizarse" al menos una vez al día, por lo que estoy usando MemoryCache.
Sé que el siguiente código no debe ser seguro para subprocesos, pero no puedo lograr que falle bajo una gran carga y para complicar las cosas, una búsqueda en Google muestra las implementaciones en ambos sentidos (con o sin bloqueos combinados con debates sean o no necesarios).
¿Alguien con conocimiento de MemoryCache en un entorno de múltiples hilos me permite saber definitivamente si necesito bloquearlo donde sea apropiado para que una llamada para eliminar (que rara vez se llama pero es un requisito) no arroje durante la recuperación / repoblación.
public class MemoryCacheService : IMemoryCacheService
{
private const string PunctuationMapCacheKey = "punctuationMaps";
private static readonly ObjectCache Cache;
private readonly IAdoNet _adoNet;
static MemoryCacheService()
{
Cache = MemoryCache.Default;
}
public MemoryCacheService(IAdoNet adoNet)
{
_adoNet = adoNet;
}
public void ClearPunctuationMaps()
{
Cache.Remove(PunctuationMapCacheKey);
}
public IEnumerable GetPunctuationMaps()
{
if (Cache.Contains(PunctuationMapCacheKey))
{
return (IEnumerable) Cache.Get(PunctuationMapCacheKey);
}
var punctuationMaps = GetPunctuationMappings();
if (punctuationMaps == null)
{
throw new ApplicationException("Unable to retrieve punctuation mappings from the database.");
}
if (punctuationMaps.Cast<IPunctuationMapDto>().Any(p => p.UntaggedValue == null || p.TaggedValue == null))
{
throw new ApplicationException("Null values detected in Untagged or Tagged punctuation mappings.");
}
// Store data in the cache
var cacheItemPolicy = new CacheItemPolicy
{
AbsoluteExpiration = DateTime.Now.AddDays(1.0)
};
Cache.AddOrGetExisting(PunctuationMapCacheKey, punctuationMaps, cacheItemPolicy);
return punctuationMaps;
}
//Go oldschool ADO.NET to break the dependency on the entity framework and need to inject the database handler to populate cache
private IEnumerable GetPunctuationMappings()
{
var table = _adoNet.ExecuteSelectCommand("SELECT [id], [TaggedValue],[UntaggedValue] FROM [dbo].[PunctuationMapper]", CommandType.Text);
if (table != null && table.Rows.Count != 0)
{
return AutoMapper.Mapper.DynamicMap<IDataReader, IEnumerable<PunctuationMapDto>>(table.CreateDataReader());
}
return null;
}
}
Acaba de cargar la biblioteca de muestras para solucionar el problema de .Net 2.0.
Echa un vistazo a este repositorio:
Estoy usando la memoria caché de Redis pero también la conmutación por error o simplemente Memorycache si falta Connectionstring.
Se basa en la biblioteca LazyCache que garantiza la ejecución única de devolución de llamada para escribir en un evento de multi threading tratando de cargar y guardar datos, especialmente si la devolución de llamada es muy costosa de ejecutar.
Aunque MemoryCache es realmente seguro para subprocesos como se han especificado otras respuestas, tiene un problema común de subprocesamiento múltiple: si 2 subprocesos intentan Get
(o comprueban) el caché al mismo tiempo, ambos perderán el caché y ambos terminarán generando el resultado y ambos agregarán el resultado a la caché.
A menudo esto es indeseable: el segundo subproceso debe esperar que el primero complete y use su resultado en lugar de generar resultados dos veces.
Esta fue una de las razones por las que escribí LazyCache , un envoltorio amistoso en MemoryCache que resuelve este tipo de problemas. También está disponible en Nuget .
Como otros han dicho, MemoryCache es seguro para hilos. Sin embargo, la seguridad de los hilos de los datos almacenados en él depende exclusivamente de su uso.
Para citar a Reed Copsey de su impresionante post sobre concurrencia y el tipo ConcurrentDictionary<TKey, TValue>
. Lo cual es por supuesto aplicable aquí.
Si dos hilos llaman a esto [GetOrAdd] simultáneamente, se pueden construir fácilmente dos instancias de TValue.
Puedes imaginar que esto sería especialmente malo si TValue
es costoso de construir.
Para trabajar en esto, puede aprovechar Lazy<T>
muy fácilmente, que casualmente es muy barato de construir. Hacer esto garantiza que si entramos en una situación de multiproceso, que solo estamos creando múltiples instancias de Lazy<T>
(que es barato).
GetOrAdd()
( GetOrCreate()
en el caso de MemoryCache
) devolverá lo mismo, Lazy<T>
singular a todos los hilos, las instancias "extra" de Lazy<T>
simplemente se descartarán.
Como Lazy<T>
no hace nada hasta que se .Value
, solo se construye una instancia del objeto.
¡Ahora para algún código! A continuación se muestra un método de extensión para IMemoryCache
que implementa lo anterior. De forma arbitraria, está configurando SlidingExpiration
función de un param del método int seconds
. Pero esto es totalmente personalizable en función de sus necesidades.
Tenga en cuenta que esto es específico de las aplicaciones .netcore2.0
public static T GetOrAdd<T>(this IMemoryCache cache, string key, int seconds, Func<T> factory)
{
return cache.GetOrCreate<T>(key, entry => new Lazy<T>(() =>
{
entry.SlidingExpiration = TimeSpan.FromSeconds(seconds);
return factory.Invoke();
}).Value);
}
Llamar:
IMemoryCache cache;
var result = cache.GetOrAdd("someKey", 60, () => new object());
Para realizar esto de forma asíncrona, recomiendo utilizar la excelente AsyncLazy<T>
Stephen Toub que se encuentra en su article en MSDN. Que combina el iniciador perezoso incorporado Lazy<T>
con la promesa Task<T>
:
public class AsyncLazy<T> : Lazy<Task<T>>
{
public AsyncLazy(Func<T> valueFactory) :
base(() => Task.Factory.StartNew(valueFactory))
{ }
public AsyncLazy(Func<Task<T>> taskFactory) :
base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap())
{ }
}
Ahora la versión asincrónica de GetOrAdd()
:
public static Task<T> GetOrAddAsync<T>(this IMemoryCache cache, string key, int seconds, Func<Task<T>> taskFactory)
{
return cache.GetOrCreateAsync<T>(key, async entry => await new AsyncLazy<T>(async () =>
{
entry.SlidingExpiration = TimeSpan.FromSeconds(seconds);
return await taskFactory.Invoke();
}).Value);
}
Y finalmente, para llamar:
IMemoryCache cache;
var result = await cache.GetOrAddAsync("someKey", 60, async () => new object());
Consulte este enlace: http://msdn.microsoft.com/en-us/library/system.runtime.caching.memorycache(v=vs.110).aspx
Vaya al final de la página (o busque el texto "Seguridad del subproceso").
Ya verás:
^ Seguridad de subprocesos
Este tipo es seguro para subprocesos.
El valor predeterminado MemoryCache
proporcionado por MemoryCache
es totalmente seguro para subprocesos. Cualquier implementación personalizada que se derive de MemoryCache
puede no ser segura para subprocesos. Si usa MemoryCache
simple de MemoryCache
, es seguro para subprocesos. Examine el código fuente de mi solución de caché distribuida de código abierto para ver cómo lo uso (MemCache.cs):
https://github.com/haneytron/dache/blob/master/Dache.CacheHost/Storage/MemCache.cs