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;
}

La razón de la concurrencia problema es que varios subprocesos pueden obtener una clave null y, a continuación, intente insertar datos en la memoria caché.

Lo que habría de ser el más corto y más limpios de forma que este código concurrencia de la prueba? Me gusta seguir un buen patrón a través de mi memoria caché de código relacionado. Un enlace a un artículo en línea sería una gran ayuda.

ACTUALIZACIÓN:

Me ocurrió con este código se basa en @Scott Chamberlain de la respuesta. ¿Alguien puede encontrar cualquier rendimiento o de concurrencia problema con esto?
Si esto funciona, se ahorrarían muchos línea 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;
}
}
}
}
}
  • ¿por qué no u uso ReaderWriterLockSlim ?
  • Estoy de acuerdo con DarthVader… yo creo que lean ReaderWriterLockSlim… Pero también me gustaría usar este técnica para evitar que se try-finally declaraciones.
  • Para su versión actualizada, yo no bloqueo en una única cacheLock más, me bloqueo por llave en su lugar. Esto se puede hacer fácilmente con un Dictionary<string, object> donde la clave es la misma que utiliza en su MemoryCache y el objeto en el diccionario es sólo una base de Object de que el bloqueo de encendido. Sin embargo, dicho esto, yo recomendaría leer a través de Jon Hanna respuesta. Sin la debida profileing que pueden estar ralentizando su programa más con el bloqueo de los arrendamientos dos instancias de SomeHeavyAndExpensiveCalculation() correr y tener un resultado arrojado lejos.
  • no bloqueo en una única cacheLock. Es responsabilidad de la persona que llama para enviar un objeto a lo largo de con cacheKey. Al mismo tiempo, su Diccionario de enfoque hace que sea más automatizado.
  • Ah, no me di cuenta de que está pasando en el objeto de bloqueo. Me gustaría utilizar ConcurrentDictionary en lugar de Dictionary así que usted no necesita utilizar cualquiera de los bloqueos en él, que acaba de tomar una línea de código var cacheLock = lockDictionary.GetOrAdd(cacheKey, new Object());
  • A mí me parece que la creación de la CacheItemPolicy después de conseguir el costoso valor de caché sería más preciso. En el peor de los casos, tales como la creación de un informe de resumen que lleva 21 minutos para volver a la «cara de la cadena» (tal vez contiene el nombre de archivo de informe en formato PDF) ya sería «caducado» antes de que fuera devuelto.
  • Buen punto, he actualizado mi respuesta a hacerlo.
  • «Creo que su bloqueo de graith… inquietante.» -Darth Vader

InformationsquelleAutor Allan Xu | 2014-01-21

6 Comentarios

  1. 82

    Esta es mi 2ª iteración del código. Porque MemoryCache es seguro para subprocesos no necesita de bloqueo en la parte inicial de lectura, puedes leer y si la caché devuelve null, a continuación, hacer el bloqueo de comprobar para ver si usted necesita para crear la cadena. Esto simplifica mucho el código.

    const string CacheKey = "CacheKey";
    static readonly object cacheLock = new object();
    private static string GetCachedData()
    {
    //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
    var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;
    if (cachedString != null)
    {
    return cachedString;
    }
    lock (cacheLock)
    {
    //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
    cachedString = MemoryCache.Default.Get(CacheKey, null) as string;
    if (cachedString != null)
    {
    return cachedString;
    }
    //The value still did not exist so we now write it in to the cache.
    var expensiveString = SomeHeavyAndExpensiveCalculation();
    CacheItemPolicy cip = new CacheItemPolicy()
    {
    AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
    };
    MemoryCache.Default.Set(CacheKey, expensiveString, cip);
    return expensiveString;
    }
    }

    EDITAR: El código de abajo, es innecesario pero yo quería salir a mostrar el método original. Puede ser útil para los futuros visitantes que están usando una colección diferente que ha hilo de seguro lee pero no seguro para subprocesos escribe (casi todos los de las clases bajo la System.Collections espacio de nombres es así).

    Aquí es cómo iba a hacerlo con ReaderWriterLockSlim para proteger el acceso. Usted necesita hacer una especie de «Registrado El Doble Bloqueo» a ver si alguien se creó el elemento en caché mientras estábamos esperando para tomar el bloqueo.

    const string CacheKey = "CacheKey";
    static readonly ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
    static string GetCachedData()
    {
    //First we do a read lock to see if it already exists, this allows multiple readers at the same time.
    cacheLock.EnterReadLock();
    try
    {
    //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
    var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;
    if (cachedString != null)
    {
    return cachedString;
    }
    }
    finally
    {
    cacheLock.ExitReadLock();
    }
    //Only one UpgradeableReadLock can exist at one time, but it can co-exist with many ReadLocks
    cacheLock.EnterUpgradeableReadLock();
    try
    {
    //We need to check again to see if the string was created while we where waiting to enter the EnterUpgradeableReadLock
    var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;
    if (cachedString != null)
    {
    return cachedString;
    }
    //The entry still does not exist so we need to create it and enter the write lock
    var expensiveString = SomeHeavyAndExpensiveCalculation();
    cacheLock.EnterWriteLock(); //This will block till all the Readers flush.
    try
    {
    CacheItemPolicy cip = new CacheItemPolicy()
    {
    AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
    };
    MemoryCache.Default.Set(CacheKey, expensiveString, cip);
    return expensiveString;
    }
    finally 
    {
    cacheLock.ExitWriteLock();
    }
    }
    finally
    {
    cacheLock.ExitUpgradeableReadLock();
    }
    }
    • El doble Check de Bloqueo no siempre funciona bien.
    • en qué manera el código anterior no funciona? también esto no es estrictamente «registrado el doble bloqueo» estoy siguiendo un patrón similar y fue la mejor manera que podía pensar para describirlo. Es por eso que me dijo que era un registrado el doble bloqueo.
    • Yo no comentar el código. Me estaba comentando que el Doble Check de Bloqueo No funciona. El código está bien.
    • +1 para el código actualizado.
    • Me resulta difícil ver qué situaciones este tipo de bloqueo, y este tipo de almacenamiento tendría sentido en si: Si eres de bloqueo en todas las creaciones de los valores de entrar en un MemoryCache las posibilidades son que al menos una de las dos cosas estaba equivocado.
    • sólo mirando este código, y no es susceptible a una excepción entre la adquisición de la cerradura y el bloque try. El autor de C# En pocas palabras describe esta aquí, albahari.com/threading/part2.aspx#_MonitorEnter_and_MonitorExit
    • Sí, es posible, sin embargo el tipo de excepciones que podía hacer que suceda probablemente sería muy malas situaciones me gustaría que el programa para derribar el dominio de aplicación, así que por eso yo no lo hice.
    • Una desventaja de este código es que CacheKey «a» bloquear una solicitud para CacheKey «B» si ambos no están en caché todavía. Para resolver esto, usted podría usar un concurrentDictionary<string,object> en la que se almacena la cachekeys bloquear en

  2. 37

    No es una librería de código libre [descargo de responsabilidad: que escribí]: LazyCache que la OMI cubre sus necesidades con dos líneas de código:

    IAppCache cache = new CachingService();
    var cachedResults = cache.GetOrAdd("CacheKey", 
    () => SomeHeavyAndExpensiveCalculation());

    Se ha construido en el bloqueo por defecto, de modo que el cacheable método sólo se ejecutará una vez por la caché, y utiliza una lambda de modo que usted puede obtener o añadir» de una sola vez. El valor predeterminado es de 20 minutos, el plazo de caducidad.

    Incluso hay un paquete de NuGet 😉

    • de aspecto agradable de la biblioteca, bien escrito docs demasiado. gracias!
    • El Apuesto de almacenamiento en caché.
    • Esto me permite ser un perezoso desarrollador que hace de esta la mejor respuesta!
    • Vale la pena mencionar el artículo que la página de github para LazyCache puntos es una buena lectura para las razones detrás de él. alastaircrabtree.com/…
  3. 31

    He resuelto este problema mediante el uso de la AddOrGetExisting método en el MemoryCache y el uso de La inicialización perezosa.

    Esencialmente, mi código se ve algo como esto:

    static string GetCachedData(string key, DateTimeOffset offset)
    {
    Lazy<String> lazyObject = new Lazy<String>(() => SomeHeavyAndExpensiveCalculationThatReturnsAString());
    var returnedLazyObject = MemoryCache.Default.AddOrGetExisting(key, lazyObject, offset); 
    if (returnedLazyObject == null)
    return lazyObject.Value;
    return ((Lazy<String>) returnedLazyObject).Value;
    }

    Peor de los casos aquí es que puedes crear el mismo Lazy objeto dos veces. Pero que es bastante trivial. El uso de AddOrGetExisting garantiza que sólo se va a conseguir nunca una instancia de la Lazy objeto, y así que también están garantizados para llamar sólo el costoso método de inicialización de una vez.

    • El problema con este tipo de enfoque es que se puede insertar datos no válidos. Si SomeHeavyAndExpensiveCalculationThatResultsAString() lanzó una excepción, que se ha atascado en la caché. Incluso excepciones transitorias recibirá en caché con Lazy<T>: msdn.microsoft.com/en-us/library/vstudio/dd642331.aspx
    • Mientras que es cierto que Perezoso<T> puede devolver un error si la inicialización excepción falla, que es bastante fácil de detectar. Luego puede desalojar a los Perezosos<T> que se resuelve en un error de la memoria caché, crear un nuevo Perezoso<T>, puesto que en la memoria caché, y resolverlo. En nuestro propio código, podemos hacer algo similar. Se vuelva a intentar un cierto número de veces antes de tirar un error.
    • AddOrGetExisting devolver null si el elemento no estaba presente, así que usted debe comprobar y volver lazyObject en ese caso
    • El Uso De LazyThreadSafetyMode.PublicationOnly evitar el almacenamiento en caché de las excepciones.
    • De acuerdo a los comentarios en esta entrada del blog si es muy caro para inicializar la entrada de la caché, es mejor sólo desalojar a una excepción (como se muestra en el ejemplo en el post del blog) en vez de usar PublicationOnly, porque hay una posibilidad de que todos los hilos se puede llamar el inicializador al mismo tiempo.
    • pregunta estúpida, pero ¿cómo utilizar LazyThreadSafetyMode.PublicationOnly
    • You can then evict any Lazy<T> that resolves to an error from the cache, create a new Lazy<T>, put that in the cache, and resolve it Como alternativa, considere el uso de LazyWithNoExceptionCachingstackoverflow.com/a/42567351/34092 para evitar la necesidad de hacer eso.
    • LazyCache tiene lógica para controlar el escenario accidental de almacenamiento en caché de una excepción nuevo ahorro de un par de líneas de código

  4. 15

    Supongo que este código tiene problemas de concurrencia:

    De hecho, es posiblemente una multa, aunque con una posible mejora.

    Ahora, en general, el patrón donde tenemos múltiples hilos de ajuste de un valor compartido en el primer uso, para que no se trabe en el valor que se obtiene y puede ser:

    1. Desastroso – otros códigos se supone que solo existe una instancia.
    2. Desastroso – el código que obtiene la instancia no se puede tolerar sólo uno (o tal vez una pequeña cantidad) operaciones simultáneas.
    3. Desastroso – los medios de almacenamiento no es thread-safe (por ejemplo, tener dos hilos que se añade al diccionario y usted puede obtener todo tipo de desagradables errores).
    4. Sub-óptimo, el rendimiento en general es peor que si el bloqueo se había asegurado de que sólo un subproceso hizo el trabajo de la obtención del valor.
    5. El óptimo, el costo de tener varios hilos de hacer el trabajo redundante es menor que el costo de la prevención, especialmente desde que sólo puede ocurrir durante un período relativamente breve.

    Sin embargo, considerando que MemoryCache puede desalojar entradas:

    1. Si es desastroso para tener más de una instancia MemoryCache es el enfoque equivocado.
    2. Si usted debe evitar la creación simultánea, debe hacerlo en el momento de la creación.
    3. MemoryCache es thread-safe, en términos de acceso a ese objeto, por lo que no es una preocupación aquí.

    Tanto de estas posibilidades han de ser pensado acerca de curso, aunque la única vez tener dos instancias de la misma cadena existente puede ser un problema si usted está haciendo muy particular optimizaciones que no se aplican aquí*.

    Así, nos quedamos con las posibilidades:

    1. Es más barato para evitar el costo de llamadas duplicadas para SomeHeavyAndExpensiveCalculation().
    2. Es más barato, no para evitar el costo de las llamadas duplicadas para SomeHeavyAndExpensiveCalculation().

    Y de trabajo que puede ser difícil (de hecho, el tipo de cosa, donde vale la pena perfiles en lugar de asumir que usted puede trabajar hacia fuera). Vale la pena considerar aquí a pesar de que la mayoría de las formas obvias de bloqueo en insertar evitará todos adiciones a la caché, incluyendo aquellos que no están relacionados.

    Esto significa que si contamos con 50 hilos tratando de 50 valores diferentes, entonces vamos a tener que hacer todos los 50 hilos esperar el uno del otro, aunque no estaban ni siquiera se va a hacer el mismo cálculo.

    Como tal, usted está probablemente mejor con el código, 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 sea necesario controlar que en algún otro lugar, o si necesita una diferente estrategia de almacenamiento en caché de uno que expulsa a las entradas antiguas†.

    La única cosa que cambiaría es que me gustaría sustituir la llamada a Set() con uno a AddOrGetExisting(). De lo anterior debe quedar claro que probablemente no sea necesario, pero no iba a permitir que el recién obtenido elemento para ser recogidos, reduciendo el uso de memoria total y permitiendo una mayor proporción de la generación de baja a alta generación de colecciones.

    Así que sí, se podría utilizar la doble bloqueo para evitar la concurrencia, pero la concurrencia no es realmente un problema, o sus almacenar los valores en el camino equivocado, o bloqueo doble en la tienda no sería la mejor manera de resolverlo.

    *Si usted sabe que sólo uno de cada uno de un conjunto de cadenas que existe, puede optimizar comparaciones de igualdad, que es el único tiempo que tienen dos copias de una cadena puede ser incorrecta en lugar de sub-óptima, pero que te gustaría estar haciendo muy diferentes tipos de almacenamiento en caché para que tenga sentido. E. g. el tipo XmlReader hace internamente.

    †Bastante probable que sea uno que almacena indefinidamente, o uno que hace uso de referencias débiles, por lo que sólo expulsar a las entradas si no hay usos existentes.

  5. 1

    Ejemplo de la consola de de MemoryCache, «Cómo guardar/recibir simples objetos de la clase»

    De salida después de su lanzamiento y pulsando Cualquier tecla excepto Esc :

    De guardar en caché!

    Llegar desde la caché!

    Some1

    Some2

        class Some
    {
    public String text { get; set; }
    public Some(String text)
    {
    this.text = text;
    }
    public override string ToString()
    {
    return text;
    }
    }
    public static MemoryCache cache = new MemoryCache("cache");
    public static string cache_name = "mycache";
    static void Main(string[] args)
    {
    Some some1 = new Some("some1");
    Some some2 = new Some("some2");
    List<Some> list = new List<Some>();
    list.Add(some1);
    list.Add(some2);
    do {
    if (cache.Contains(cache_name))
    {
    Console.WriteLine("Getting from cache!");
    List<Some> list_c = cache.Get(cache_name) as List<Some>;
    foreach (Some s in list_c) Console.WriteLine(s);
    }
    else
    {
    Console.WriteLine("Saving to cache!");
    cache.Set(cache_name, list, DateTime.Now.AddMinutes(10));                   
    }
    } while (Console.ReadKey(true).Key != ConsoleKey.Escape);
    }
  6. 1
    public interface ILazyCacheProvider : IAppCache
    {
    ///<summary>
    ///Get data loaded - after allways throw cached result (even when data is older then needed) but very fast!
    ///</summary>
    ///<param name="key"></param>
    ///<param name="getData"></param>
    ///<param name="slidingExpiration"></param>
    ///<typeparam name="T"></typeparam>
    ///<returns></returns>
    T GetOrAddPermanent<T>(string key, Func<T> getData, TimeSpan slidingExpiration);
    }
    ///<summary>
    ///Initialize LazyCache in runtime
    ///</summary>
    public class LazzyCacheProvider: CachingService, ILazyCacheProvider
    {
    private readonly Logger _logger = LogManager.GetLogger("MemCashe");
    private readonly Hashtable _hash = new Hashtable();
    private readonly List<string>  _reloader = new List<string>();
    private readonly ConcurrentDictionary<string, DateTime> _lastLoad = new ConcurrentDictionary<string, DateTime>();  
    T ILazyCacheProvider.GetOrAddPermanent<T>(string dataKey, Func<T> getData, TimeSpan slidingExpiration)
    {
    var currentPrincipal = Thread.CurrentPrincipal;
    if (!ObjectCache.Contains(dataKey) && !_hash.Contains(dataKey))
    {
    _hash[dataKey] = null;
    _logger.Debug($"{dataKey} - first start");
    _lastLoad[dataKey] = DateTime.Now;
    _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
    _lastLoad[dataKey] = DateTime.Now;
    _logger.Debug($"{dataKey} - first");
    }
    else
    {
    if ((!ObjectCache.Contains(dataKey) || _lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) < DateTime.Now) && _hash[dataKey] != null)
    Task.Run(() =>
    {
    if (_reloader.Contains(dataKey)) return;
    lock (_reloader)
    {
    if (ObjectCache.Contains(dataKey))
    {
    if(_lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) > DateTime.Now)
    return;
    _lastLoad[dataKey] = DateTime.Now;
    Remove(dataKey);
    }
    _reloader.Add(dataKey);
    Thread.CurrentPrincipal = currentPrincipal;
    _logger.Debug($"{dataKey} - reload start");
    _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
    _logger.Debug($"{dataKey} - reload");
    _reloader.Remove(dataKey);
    }
    });
    }
    if (_hash[dataKey] != null) return (T) (_hash[dataKey]);
    _logger.Debug($"{dataKey} - dummy start");
    var data = GetOrAdd(dataKey, getData, slidingExpiration);
    _logger.Debug($"{dataKey} - dummy");
    return (T)((object)data).CloneObject();
    }
    }
    • Muy rápido LazyCache 🙂 escribí este código para la API de REST de repositorios.

Dejar respuesta

Please enter your comment!
Please enter your name here