Para empezar, déjeme decir que sé que el siguiente código no es seguro para subprocesos (corrección: podría serlo). Con lo que estoy luchando es con encontrar una implementación que sea y una que realmente pueda fallar bajo prueba. Estoy refactorizando un gran proyecto WCF en este momento que necesita algunos datos estáticos (en su mayoría) almacenados en caché y se rellenan 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 código a continuación no debería ser seguro para subprocesos, pero no puedo hacer que falle bajo una carga pesada y para complicar las cosas, una búsqueda en Google muestra implementaciones en ambos sentidos (con y sin bloqueos combinados con debates sobre si son necesarios o no).
¿Podría alguien con conocimiento de MemoryCache en un entorno de subprocesos múltiples dejarme saber definitivamente si necesito o no bloquear donde sea apropiado para que una llamada para eliminar (que rara vez se llamará pero es un requisito) no se lanzará 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;
}
}
fuente
Respuestas:
El MS predeterminado proporcionado
MemoryCache
es completamente seguro para subprocesos. Cualquier implementación personalizada que se derive deMemoryCache
puede no ser segura para subprocesos. Si está usando simpleMemoryCache
fuera de la caja, es seguro para subprocesos. Examine el código fuente de mi solución de almacenamiento en caché distribuido de código abierto para ver cómo lo uso (MemCache.cs):https://github.com/haneytron/dache/blob/master/Dache.CacheHost/Storage/MemCache.cs
fuente
GetOrCreate
método. hay un problema sobre eso en githubSi bien MemoryCache es seguro para subprocesos, como han especificado otras respuestas, tiene un problema común de subprocesos múltiples: si 2 subprocesos intentan salir
Get
(o verificarContains
) 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 no es deseable: el segundo hilo debe esperar a que se complete el primero y usar su resultado en lugar de generar resultados dos veces.
Esta fue una de las razones por las que escribí LazyCache , un contenedor amigable en MemoryCache que resuelve este tipo de problemas. También está disponible en Nuget .
fuente
AddOrGetExisting
, en lugar de implementar lógica personalizadaContains
. ElAddOrGetExisting
método de MemoryCache es referenciasComo han dicho otros, MemoryCache es seguro para subprocesos. Sin embargo, la seguridad de los subprocesos de los datos almacenados en él depende completamente de su uso.
Para citar a Reed Copsey de su impresionante publicación sobre la concurrencia y el
ConcurrentDictionary<TKey, TValue>
tipo. Lo cual, por supuesto, es aplicable aquí.Puede imaginar que esto sería especialmente malo si
TValue
su construcción es costosa.Para solucionar esto, puede aprovechar
Lazy<T>
muy fácilmente, lo que casualmente es muy barato de construir. Hacer esto asegura que si nos metemos en una situación de multiproceso, solo estamos creando múltiples instancias deLazy<T>
(lo cual es barato).GetOrAdd()
(GetOrCreate()
en el caso deMemoryCache
) devolverá lo mismo, singularLazy<T>
a todos los subprocesos, las instancias "extra" deLazy<T>
simplemente se desechan.Dado
Lazy<T>
que no hace nada hasta que.Value
se llama, solo se construye una instancia del objeto.¡Ahora un poco de código! A continuación se muestra un método de extensión para el
IMemoryCache
que implementa lo anterior. Se establece arbitrariamente enSlidingExpiration
función de un parámetro deint seconds
método. Pero esto es completamente personalizable según sus necesidades.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 todo esto de forma asincrónica, recomiendo utilizar la excelente
AsyncLazy<T>
implementación de Stephen Toub que se encuentra en su artículo sobre MSDN. Que combina el inicializador perezoso incorporadoLazy<T>
con la promesaTask<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, llamar:
IMemoryCache cache; var result = await cache.GetOrAddAsync("someKey", 60, async () => new object());
fuente
MemoryCache.GetOrCreate
no es seguro para subprocesos de la misma manera que loConcurrentDictionary
esLazy
? Si es así, ¿cómo validaste esto?GetOrCreate
usar la misma clave y esa fábrica. el resultado, la fábrica se usó 10 veces cuando se usó con memoria caché (vi las impresiones) + ¡cada vez queGetOrCreate
devolvió un valor diferente! Ejecuté la misma prueba usandoConcurrentDicionary
y vi que la fábrica se usaba solo una vez, y obtuve el mismo valor siempre. Encontré un problema cerrado en github, acabo de escribir un comentario allí de que debería ser reabiertoConsulte 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 para subprocesos").
Ya verás:
fuente
MemoryCache.Default
un volumen muy alto (millones de accesos de caché por minuto) sin problemas de subprocesos todavía.Se acaba de cargar la biblioteca de muestra para solucionar el problema de .Net 2.0.
Eche un vistazo a este repositorio:
RedisLazyCache
Estoy usando la caché de Redis, pero también la conmutación por error o solo la memoria caché si falta Connectionstring.
Se basa en la biblioteca LazyCache que garantiza la ejecución única de devolución de llamada para escritura en un evento de subprocesos múltiples que intentan cargar y guardar datos, especialmente si la devolución de llamada es muy costosa de ejecutar.
fuente
Como lo mencionó @AmitE en la respuesta de @pimbrouwers, su ejemplo no funciona como se demuestra aquí:
class Program { static async Task Main(string[] args) { var cache = new MemoryCache(new MemoryCacheOptions()); var tasks = new List<Task>(); var counter = 0; for (int i = 0; i < 10; i++) { var loc = i; tasks.Add(Task.Run(() => { var x = GetOrAdd(cache, "test", TimeSpan.FromMinutes(1), () => Interlocked.Increment(ref counter)); Console.WriteLine($"Interation {loc} got {x}"); })); } await Task.WhenAll(tasks); Console.WriteLine("Total value creations: " + counter); Console.ReadKey(); } public static T GetOrAdd<T>(IMemoryCache cache, string key, TimeSpan expiration, Func<T> valueFactory) { return cache.GetOrCreate(key, entry => { entry.SetSlidingExpiration(expiration); return new Lazy<T>(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication); }).Value; } }
Salida:
Interation 6 got 8 Interation 7 got 6 Interation 2 got 3 Interation 3 got 2 Interation 4 got 10 Interation 8 got 9 Interation 5 got 4 Interation 9 got 1 Interation 1 got 5 Interation 0 got 7 Total value creations: 10
Parece que
GetOrCreate
siempre devuelve la entrada creada. Afortunadamente, eso es muy fácil de solucionar:public static T GetOrSetValueSafe<T>(IMemoryCache cache, string key, TimeSpan expiration, Func<T> valueFactory) { if (cache.TryGetValue(key, out Lazy<T> cachedValue)) return cachedValue.Value; cache.GetOrCreate(key, entry => { entry.SetSlidingExpiration(expiration); return new Lazy<T>(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication); }); return cache.Get<Lazy<T>>(key).Value; }
Eso funciona como se esperaba:
Interation 4 got 1 Interation 9 got 1 Interation 1 got 1 Interation 8 got 1 Interation 0 got 1 Interation 6 got 1 Interation 7 got 1 Interation 2 got 1 Interation 5 got 1 Interation 3 got 1 Total value creations: 1
fuente
El caché es seguro para subprocesos, pero como otros han dicho, es posible que GetOrAdd llame a la función de varios tipos si llama de varios tipos.
Aquí está mi solución mínima en eso
private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1);
y
await _cacheLock.WaitAsync(); var data = await _cache.GetOrCreateAsync(key, entry => ...); _cacheLock.Release();
fuente