Supongo que este código tiene problemas de concurrencia:
const string CacheKey = "CacheKey";
static string GetCachedData()
{
string expensiveString =null;
if (MemoryCache.Default.Contains(CacheKey))
{
expensiveString = MemoryCache.Default[CacheKey] as string;
}
else
{
CacheItemPolicy cip = new CacheItemPolicy()
{
AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
};
expensiveString = SomeHeavyAndExpensiveCalculation();
MemoryCache.Default.Set(CacheKey, expensiveString, cip);
}
return expensiveString;
}
El motivo del problema de concurrencia es que varios subprocesos pueden obtener una clave nula y luego intentar insertar datos en la caché.
¿Cuál sería la forma más corta y limpia de hacer que este código sea a prueba de concurrencia? Me gusta seguir un buen patrón en mi código relacionado con la caché. Un enlace a un artículo en línea sería de gran ayuda.
ACTUALIZAR:
Se me ocurrió este código basado en la respuesta de @Scott Chamberlain. ¿Alguien puede encontrar algún problema de rendimiento o concurrencia con esto? Si esto funciona, se ahorrarían muchas líneas de código y errores.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Caching;
namespace CachePoc
{
class Program
{
static object everoneUseThisLockObject4CacheXYZ = new object();
const string CacheXYZ = "CacheXYZ";
static object everoneUseThisLockObject4CacheABC = new object();
const string CacheABC = "CacheABC";
static void Main(string[] args)
{
string xyzData = MemoryCacheHelper.GetCachedData<string>(CacheXYZ, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
string abcData = MemoryCacheHelper.GetCachedData<string>(CacheABC, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
}
private static string SomeHeavyAndExpensiveXYZCalculation() {return "Expensive";}
private static string SomeHeavyAndExpensiveABCCalculation() {return "Expensive";}
public static class MemoryCacheHelper
{
public static T GetCachedData<T>(string cacheKey, object cacheLock, int cacheTimePolicyMinutes, Func<T> GetData)
where T : class
{
//Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
T cachedData = MemoryCache.Default.Get(cacheKey, null) as T;
if (cachedData != null)
{
return cachedData;
}
lock (cacheLock)
{
//Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
cachedData = MemoryCache.Default.Get(cacheKey, null) as T;
if (cachedData != null)
{
return cachedData;
}
//The value still did not exist so we now write it in to the cache.
CacheItemPolicy cip = new CacheItemPolicy()
{
AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(cacheTimePolicyMinutes))
};
cachedData = GetData();
MemoryCache.Default.Set(cacheKey, cachedData, cip);
return cachedData;
}
}
}
}
}
c#
.net
multithreading
memorycache
Allan Xu
fuente
fuente
ReaderWriterLockSlim
?ReaderWriterLockSlim
... Pero también usaría esta técnica para evitartry-finally
declaraciones.Dictionary<string, object>
donde la clave es la misma clave que usa en suMemoryCache
y el objeto en el diccionario es solo un básicoObject
que bloquea. Sin embargo, dicho esto, te recomiendo que leas la respuesta de Jon Hanna. Sin un perfil adecuado, es posible que ralentice su programa más con el bloqueo que dejando que se ejecuten dos instanciasSomeHeavyAndExpensiveCalculation()
y se descarte un resultado.Respuestas:
Esta es mi segunda iteración del código. Debido a que
MemoryCache
es seguro para subprocesos, no necesita bloquear la lectura inicial, solo puede leer y si el caché devuelve nulo, haga la verificación de bloqueo para ver si necesita crear la cadena. Simplifica enormemente el código.EDITAR : El siguiente código es innecesario, pero quería dejarlo para mostrar el método original. Puede ser útil para futuros visitantes que estén usando una colección diferente que tenga lecturas seguras para subprocesos pero escrituras no seguras para subprocesos (casi todas las clases bajo el
System.Collections
espacio de nombres son así).Así es como lo haría usando
ReaderWriterLockSlim
para proteger el acceso. Necesita hacer una especie de " Bloqueo de doble verificación" para ver si alguien más creó el elemento almacenado en caché mientras estábamos esperando para tomar el bloqueo.fuente
MemoryCache
probable que al menos una de esas dos cosas esté mal.Hay una biblioteca de código abierto [descargo de responsabilidad: que escribí]: LazyCache que IMO cubre su requisito con dos líneas de código:
Tiene un bloqueo integrado de forma predeterminada, por lo que el método almacenable en caché solo se ejecutará una vez por cada error de caché, y usa una lambda para que pueda "obtener o agregar" de una vez. El valor predeterminado es una expiración deslizante de 20 minutos.
Incluso hay un paquete NuGet ;)
fuente
Resolví este problema haciendo uso del método AddOrGetExisting en MemoryCache y el uso de la inicialización diferida .
Esencialmente, mi código se parece a esto:
El peor de los casos aquí es que crea el mismo
Lazy
objeto dos veces. Pero eso es bastante trivial. El uso deAddOrGetExisting
garantías de que solo obtendrá una instancia delLazy
objeto, por lo que también está garantizado que solo llamará al costoso método de inicialización una vez.fuente
SomeHeavyAndExpensiveCalculationThatResultsAString()
arrojó una excepción, está atascado en la caché. Incluso las excepciones transitorias se almacenarán en caché conLazy<T>
: msdn.microsoft.com/en-us/library/vstudio/dd642331.aspxEn realidad, es muy posible que esté bien, aunque con una posible mejora.
Ahora, en general, el patrón en el que tenemos varios subprocesos configurando un valor compartido en el primer uso, para no bloquear el valor que se obtiene y establece puede ser:
Sin embargo, considerando aquí que
MemoryCache
puede desalojar las entradas entonces:MemoryCache
es el enfoque incorrecto.MemoryCache
es seguro para subprocesos en términos de acceso a ese objeto, por lo que no es una preocupación aquí.Por supuesto, hay que pensar en ambas posibilidades, aunque la única vez que existen dos instancias de la misma cadena puede ser un problema es si está haciendo optimizaciones muy particulares que no se aplican aquí *.
Entonces, nos quedamos con las posibilidades:
SomeHeavyAndExpensiveCalculation()
.SomeHeavyAndExpensiveCalculation()
.Y resolver eso puede ser difícil (de hecho, el tipo de cosas en las que vale la pena perfilar en lugar de asumir que puede resolverlo). Sin embargo, vale la pena considerar aquí que las formas más obvias de bloquear la inserción evitarán todas las adiciones a la caché, incluidas las que no están relacionadas.
Esto significa que si tuviéramos 50 subprocesos tratando de establecer 50 valores diferentes, entonces tendremos que hacer que los 50 subprocesos se esperen entre sí, aunque ni siquiera iban a hacer el mismo cálculo.
Como tal, probablemente esté mejor con el código que tiene, que con el código que evita la condición de carrera, y si la condición de carrera es un problema, es muy probable que deba manejar eso en otro lugar o necesite una condición diferente. estrategia de almacenamiento en caché que una que expulsa entradas antiguas †.
Lo único que cambiaría es reemplazar la llamada a
Set()
por unaAddOrGetExisting()
. De lo anterior debería quedar claro que probablemente no sea necesario, pero permitiría recopilar el elemento recién obtenido, lo que reduciría el uso general de la memoria y permitiría una mayor proporción de colecciones de baja generación a alta.Entonces, sí, podría usar el doble bloqueo para evitar la concurrencia, pero la concurrencia no es realmente un problema, o está almacenando los valores de manera incorrecta, o el doble bloqueo en la tienda no sería la mejor manera de resolverlo. .
* Si sabe que solo existe una de cada conjunto de cadenas, puede optimizar las comparaciones de igualdad, que es la única vez que tener dos copias de una cadena puede ser incorrecto en lugar de simplemente subóptimo, pero le gustaría hacerlo tipos muy diferentes de almacenamiento en caché para que eso tenga sentido. Por ejemplo, el género lo
XmlReader
hace internamente.† Es muy probable que se almacene de forma indefinida o que haga uso de referencias débiles, por lo que solo expulsará entradas si no hay usos existentes.
fuente
Para evitar el bloqueo global, puede usar SingletonCache para implementar un bloqueo por clave, sin aumentar el uso de la memoria (los objetos de bloqueo se eliminan cuando ya no se hace referencia, y la adquisición / liberación es segura para subprocesos, lo que garantiza que solo 1 instancia esté en uso a través de comparar e intercambiar).
Usarlo se ve así:
El código está aquí en GitHub: https://github.com/bitfaster/BitFaster.Caching
También hay una implementación LRU que es más liviana que MemoryCache y tiene varias ventajas: lecturas y escrituras simultáneas más rápidas, tamaño limitado, sin hilo de fondo, contadores de rendimiento internos, etc. (descargo de responsabilidad, lo escribí).
fuente
Ejemplo de consola de MemoryCache , "Cómo guardar / obtener objetos de clase simples"
Salida después de iniciar y presionar Any keyexcepto Esc:
¡Guardando en caché!
¡Obteniendo del caché!
Algo1
Algo2
fuente
fuente
Sin embargo, es un poco tarde ... Implementación completa:
Aquí está la
getPageContent
firma:Y aquí está la
MemoryCacheWithPolicy
implementación:nlogger
es solo unnLog
objeto para rastrear elMemoryCacheWithPolicy
comportamiento. Vuelvo a crear la memoria caché si el objeto de solicitud (RequestQuery requestQuery
) se cambia a través del delegado (Func<TParameter, TResult> createCacheData
) o lo vuelvo a crear cuando el tiempo deslizante o absoluto alcanzó su límite. Tenga en cuenta que todo es asincrónico también;)fuente
Es difícil elegir cuál es mejor; lock o ReaderWriterLockSlim. Necesita estadísticas del mundo real de lectura y escritura de números y proporciones, etc.
Pero si cree que usar "bloqueo" es la forma correcta. Entonces aquí hay una solución diferente para diferentes necesidades. También incluyo la solución de Allan Xu en el código. Porque ambos pueden ser necesarios para diferentes necesidades.
Estos son los requisitos que me llevan a esta solución:
Código:
fuente