2010-02-24 4 views
31

Antecedentes:bibliotecas cache de procesos de seguridad para .NET

que mantienen varias aplicaciones Winforms y bibliotecas de clases que cualquiera podía hacer o que ya se benefician de almacenamiento en caché. También conozco el Caching Application Block y el espacio de nombres System.Web.Caching (que, por lo que he reunido, está perfectamente bien para usar fuera de ASP.NET).

He descubierto que, aunque las dos clases anteriores son técnicamente "seguras para subprocesos" en el sentido de que los métodos individuales están sincronizados, en realidad no parecen estar diseñados especialmente bien para escenarios de múltiples subprocesos. Específicamente, no implementan un GetOrAdd method similar al de la nueva clase ConcurrentDictionary en .NET 4.0.

Considero que este método es una primitiva para la funcionalidad de almacenamiento en caché/búsqueda, y obviamente los diseñadores de Framework también se dieron cuenta de esto - es por eso que los métodos existen en las colecciones concurrentes. Sin embargo, aparte del hecho de que todavía no estoy usando .NET 4.0 en aplicaciones de producción, un diccionario no es un caché completo: no tiene características como caducidad, almacenamiento persistente/distribuido, etc.


por qué esto es importante:

un diseño bastante típico en un "cliente enriquecido" aplicación (o incluso algunas aplicaciones web) es empezar a pre-carga de un caché tan pronto como se inicia la aplicación, el bloqueo si el el cliente solicita datos que aún no están cargados (posteriormente lo almacenan en caché para usarlo en el futuro). Si el usuario está trabajando en su flujo de trabajo rápidamente, o si la conexión de red es lenta, no es inusual que el cliente compita con el preloader, y realmente no tiene mucho sentido pedir los mismos datos dos veces. , especialmente si la solicitud es relativamente costosa.

Por lo tanto, parece ser dejado con algunas opciones igualmente pésimos:

  • No trate de hacer la operación atómica en absoluto, y correr el riesgo de que los datos sean cargados en dos ocasiones (y posiblemente tener dos hilos diferentes operando en diferentes copias);

  • acceso Serialize a la caché, lo que implica el bloqueo de la totalidad caché sólo para cargar un solo elemento ;

  • Comience a reinventar la rueda solo para obtener algunos métodos adicionales.


Aclaración: Ejemplo línea de tiempo

decir que cuando una aplicación se inicia, necesita cargar 3 conjuntos de datos que cada toma 10 segundos para cargar. Tenga en cuenta las siguientes dos líneas de tiempo:

 
00:00 - Start loading Dataset 1 
00:10 - Start loading Dataset 2 
00:19 - User asks for Dataset 2 

En el caso anterior, si no utilizamos ningún tipo de sincronización, el usuario tiene que esperar un total de 10 segundos para los datos que estarán disponibles en 1 segundo, porque el El código verá que el elemento aún no se ha cargado en la caché e intentará volver a cargarlo.

 
00:00 - Start loading Dataset 1 
00:10 - Start loading Dataset 2 
00:11 - User asks for Dataset 1 

En este caso, el usuario es buscar los datos que ya se encuentra en la caché. Pero si serializamos el acceso a la memoria caché, tendrá que esperar otros 9 segundos sin ningún motivo, porque el administrador de la memoria caché (sea lo que sea) no tenga conocimiento de la pregunta del elemento específico que se solicita, solo que "algo "se está solicitando y" algo "está en progreso.


La Pregunta:

¿Hay bibliotecas de caché para .NET (pre-4.0) que qué implementar dichas operaciones atómicas, como uno podría esperar de un cache de procesos de fallos?

O, alternativamente, ¿hay algún medio para extender una memoria caché existente "thread-safe" para apoyar este tipo de operaciones, sin serializar el acceso a la memoria caché (lo cual sería contrario al propósito de utilizar una aplicación segura para los subprocesos en el primer lugar)? Dudo que exista, pero tal vez estoy cansado e ignorando una solución obvia.

O ... ¿hay algo más que me falta? ¿Es una práctica estándar dejar que dos hilos de la competencia se peguen entre sí si, por casualidad, ambos solicitan el mismo artículo, al mismo tiempo, por primera vez o después de un vencimiento?

+0

Tengo curiosidad por lo que quiere decir con no serializar el acceso al caché. De una manera u otra, el acceso al mismo recurso exacto (ya sea durante la creación inicial o después de la caducidad) tendría que ser serializado. Debería ser posible serializar el acceso a través de la clave en lugar de todo el caché, pero en algún punto, tendría que requerirse una serialización ... a menos que me faltara algo ... – jrista

+0

@jrista: la serialización no es la única medios de seguridad de hilo; también hay bloqueos de lector-escritor, etc. Más al punto, sin embargo, una biblioteca segura para hilos debería manejar toda esta lógica por sí misma. Todas las implementaciones de caché que he visto son solo "thread safe" en el sentido de "múltiples hilos que invocan operaciones al mismo tiempo no pueden corromper el caché", pero lo que quiero es "soporte integrado para operaciones atómicas de múltiples pasos comunes, como carga lenta de elementos de caché ". – Aaronaught

+0

Bueno, incluso en el caso de un bloqueo de lector-escritor ... si hay un único escritor, todo, todos los demás escritores y todos los lectores, están bloqueados hasta que se libere el único escritor. Fuera de las operaciones más básicas (como incrementos/decrementos, intercambios, etc. accesibles a través de la clase Interbloqueada), la serialización es generalmente una consecuencia inevitable de la sincronización de subprocesos. Puedes esconderlo y esconderlo todo lo que quieras, pero en algún momento sucede. Si quieres un caché seguro para subprocesos que hace lo que estás buscando, es posible, pero es probable que tengas que escribirlo tú mismo ... – jrista

Respuesta

5

Sé tu dolor ya que soy uno de los arquitectos de Dedoose. He estado jugando con muchas bibliotecas de almacenamiento en caché y terminé construyendo esta después de mucha tribulación. La única suposición para este Administrador de caché es que todas las colecciones almacenadas por esta clase implementan una interfaz para obtener un Guid como una propiedad "Id" en cada objeto. Siendo que esto es para un RIA, incluye una gran cantidad de métodos para agregar/actualizar/eliminar elementos de estas colecciones.

Aquí es mi CollectionCacheManager

public class CollectionCacheManager 
{ 
    private static readonly object _objLockPeek = new object(); 
    private static readonly Dictionary<String, object> _htLocksByKey = new Dictionary<string, object>(); 
    private static readonly Dictionary<String, CollectionCacheEntry> _htCollectionCache = new Dictionary<string, CollectionCacheEntry>(); 

    private static DateTime _dtLastPurgeCheck; 

    public static List<T> FetchAndCache<T>(string sKey, Func<List<T>> fGetCollectionDelegate) where T : IUniqueIdActiveRecord 
    { 
     List<T> colItems = new List<T>(); 

     lock (GetKeyLock(sKey)) 
     { 
      if (_htCollectionCache.Keys.Contains(sKey) == true) 
      { 
       CollectionCacheEntry objCacheEntry = _htCollectionCache[sKey]; 
       colItems = (List<T>) objCacheEntry.Collection; 
       objCacheEntry.LastAccess = DateTime.Now; 
      } 
      else 
      { 
       colItems = fGetCollectionDelegate(); 
       SaveCollection<T>(sKey, colItems); 
      } 
     } 

     List<T> objReturnCollection = CloneCollection<T>(colItems); 
     return objReturnCollection; 
    } 

    public static List<Guid> FetchAndCache(string sKey, Func<List<Guid>> fGetCollectionDelegate) 
    { 
     List<Guid> colIds = new List<Guid>(); 

     lock (GetKeyLock(sKey)) 
     { 
      if (_htCollectionCache.Keys.Contains(sKey) == true) 
      { 
       CollectionCacheEntry objCacheEntry = _htCollectionCache[sKey]; 
       colIds = (List<Guid>)objCacheEntry.Collection; 
       objCacheEntry.LastAccess = DateTime.Now; 
      } 
      else 
      { 
       colIds = fGetCollectionDelegate(); 
       SaveCollection(sKey, colIds); 
      } 
     } 

     List<Guid> colReturnIds = CloneCollection(colIds); 
     return colReturnIds; 
    } 


    private static List<T> GetCollection<T>(string sKey) where T : IUniqueIdActiveRecord 
    { 
     List<T> objReturnCollection = null; 

     if (_htCollectionCache.Keys.Contains(sKey) == true) 
     { 
      CollectionCacheEntry objCacheEntry = null; 

      lock (GetKeyLock(sKey)) 
      { 
       objCacheEntry = _htCollectionCache[sKey]; 
       objCacheEntry.LastAccess = DateTime.Now; 
      } 

      if (objCacheEntry.Collection != null && objCacheEntry.Collection is List<T>) 
      { 
       objReturnCollection = CloneCollection<T>((List<T>)objCacheEntry.Collection); 
      } 
     } 

     return objReturnCollection; 
    } 


    public static void SaveCollection<T>(string sKey, List<T> colItems) where T : IUniqueIdActiveRecord 
    { 

     CollectionCacheEntry objCacheEntry = new CollectionCacheEntry(); 

     objCacheEntry.Key = sKey; 
     objCacheEntry.CacheEntry = DateTime.Now; 
     objCacheEntry.LastAccess = DateTime.Now; 
     objCacheEntry.LastUpdate = DateTime.Now; 
     objCacheEntry.Collection = CloneCollection(colItems); 

     lock (GetKeyLock(sKey)) 
     { 
      _htCollectionCache[sKey] = objCacheEntry; 
     } 
    } 

    public static void SaveCollection(string sKey, List<Guid> colIDs) 
    { 

     CollectionCacheEntry objCacheEntry = new CollectionCacheEntry(); 

     objCacheEntry.Key = sKey; 
     objCacheEntry.CacheEntry = DateTime.Now; 
     objCacheEntry.LastAccess = DateTime.Now; 
     objCacheEntry.LastUpdate = DateTime.Now; 
     objCacheEntry.Collection = CloneCollection(colIDs); 

     lock (GetKeyLock(sKey)) 
     { 
      _htCollectionCache[sKey] = objCacheEntry; 
     } 
    } 

    public static void UpdateCollection<T>(string sKey, List<T> colItems) where T : IUniqueIdActiveRecord 
    { 
     lock (GetKeyLock(sKey)) 
     { 
      if (_htCollectionCache.ContainsKey(sKey) == true) 
      { 
       CollectionCacheEntry objCacheEntry = _htCollectionCache[sKey]; 
       objCacheEntry.LastAccess = DateTime.Now; 
       objCacheEntry.LastUpdate = DateTime.Now; 
       objCacheEntry.Collection = new List<T>(); 

       //Clone the collection before insertion to ensure it can't be touched 
       foreach (T objItem in colItems) 
       { 
        objCacheEntry.Collection.Add(objItem); 
       } 

       _htCollectionCache[sKey] = objCacheEntry; 
      } 
      else 
      { 
       SaveCollection<T>(sKey, colItems); 
      } 
     } 
    } 

    public static void UpdateItem<T>(string sKey, T objItem) where T : IUniqueIdActiveRecord 
    { 
     lock (GetKeyLock(sKey)) 
     { 
      if (_htCollectionCache.ContainsKey(sKey) == true) 
      { 
       CollectionCacheEntry objCacheEntry = _htCollectionCache[sKey]; 
       List<T> colItems = (List<T>)objCacheEntry.Collection; 

       colItems.RemoveAll(o => o.Id == objItem.Id); 
       colItems.Add(objItem); 

       objCacheEntry.Collection = colItems; 

       objCacheEntry.LastAccess = DateTime.Now; 
       objCacheEntry.LastUpdate = DateTime.Now; 
      } 
     } 
    } 

    public static void UpdateItems<T>(string sKey, List<T> colItemsToUpdate) where T : IUniqueIdActiveRecord 
    { 
     lock (GetKeyLock(sKey)) 
     { 
      if (_htCollectionCache.ContainsKey(sKey) == true) 
      { 
       CollectionCacheEntry objCacheEntry = _htCollectionCache[sKey]; 
       List<T> colCachedItems = (List<T>)objCacheEntry.Collection; 

       foreach (T objItem in colItemsToUpdate) 
       { 
        colCachedItems.RemoveAll(o => o.Id == objItem.Id); 
        colCachedItems.Add(objItem); 
       } 

       objCacheEntry.Collection = colCachedItems; 

       objCacheEntry.LastAccess = DateTime.Now; 
       objCacheEntry.LastUpdate = DateTime.Now; 
      } 
     } 
    } 

    public static void RemoveItemFromCollection<T>(string sKey, T objItem) where T : IUniqueIdActiveRecord 
    { 
     lock (GetKeyLock(sKey)) 
     { 
      List<T> objCollection = GetCollection<T>(sKey); 
      if (objCollection != null && objCollection.Count(o => o.Id == objItem.Id) > 0) 
      { 
       objCollection.RemoveAll(o => o.Id == objItem.Id); 
       UpdateCollection<T>(sKey, objCollection); 
      } 
     } 
    } 

    public static void RemoveItemsFromCollection<T>(string sKey, List<T> colItemsToAdd) where T : IUniqueIdActiveRecord 
    { 
     lock (GetKeyLock(sKey)) 
     { 
      Boolean bCollectionChanged = false; 

      List<T> objCollection = GetCollection<T>(sKey); 
      foreach (T objItem in colItemsToAdd) 
      { 
       if (objCollection != null && objCollection.Count(o => o.Id == objItem.Id) > 0) 
       { 
        objCollection.RemoveAll(o => o.Id == objItem.Id); 
        bCollectionChanged = true; 
       } 
      } 
      if (bCollectionChanged == true) 
      { 
       UpdateCollection<T>(sKey, objCollection); 
      } 
     } 
    } 

    public static void AddItemToCollection<T>(string sKey, T objItem) where T : IUniqueIdActiveRecord 
    { 
     lock (GetKeyLock(sKey)) 
     { 
      List<T> objCollection = GetCollection<T>(sKey); 
      if (objCollection != null && objCollection.Count(o => o.Id == objItem.Id) == 0) 
      { 
       objCollection.Add(objItem); 
       UpdateCollection<T>(sKey, objCollection); 
      } 
     } 
    } 

    public static void AddItemsToCollection<T>(string sKey, List<T> colItemsToAdd) where T : IUniqueIdActiveRecord 
    { 
     lock (GetKeyLock(sKey)) 
     { 
      List<T> objCollection = GetCollection<T>(sKey); 
      Boolean bCollectionChanged = false; 
      foreach (T objItem in colItemsToAdd) 
      { 
       if (objCollection != null && objCollection.Count(o => o.Id == objItem.Id) == 0) 
       { 
        objCollection.Add(objItem); 
        bCollectionChanged = true; 
       } 
      } 
      if (bCollectionChanged == true) 
      { 
       UpdateCollection<T>(sKey, objCollection); 
      } 
     } 
    } 

    public static void PurgeCollectionByMaxLastAccessInMinutes(int iMinutesSinceLastAccess) 
    { 
     DateTime dtThreshHold = DateTime.Now.AddMinutes(iMinutesSinceLastAccess * -1); 

     if (_dtLastPurgeCheck == null || dtThreshHold > _dtLastPurgeCheck) 
     { 

      lock (_objLockPeek) 
      { 
       CollectionCacheEntry objCacheEntry; 
       List<String> colKeysToRemove = new List<string>(); 

       foreach (string sCollectionKey in _htCollectionCache.Keys) 
       { 
        objCacheEntry = _htCollectionCache[sCollectionKey]; 
        if (objCacheEntry.LastAccess < dtThreshHold) 
        { 
         colKeysToRemove.Add(sCollectionKey); 
        } 
       } 

       foreach (String sKeyToRemove in colKeysToRemove) 
       { 
        _htCollectionCache.Remove(sKeyToRemove); 
       } 
      } 

      _dtLastPurgeCheck = DateTime.Now; 
     } 
    } 

    public static void ClearCollection(String sKey) 
    { 
     lock (GetKeyLock(sKey)) 
     { 
      lock (_objLockPeek) 
      { 
       if (_htCollectionCache.ContainsKey(sKey) == true) 
       { 
        _htCollectionCache.Remove(sKey); 
       } 
      } 
     } 
    } 


    #region Helper Methods 
    private static object GetKeyLock(String sKey) 
    { 
     //Ensure even if hell freezes over this lock exists 
     if (_htLocksByKey.Keys.Contains(sKey) == false) 
     { 
      lock (_objLockPeek) 
      { 
       if (_htLocksByKey.Keys.Contains(sKey) == false) 
       { 
        _htLocksByKey[sKey] = new object(); 
       } 
      } 
     } 

     return _htLocksByKey[sKey]; 
    } 

    private static List<T> CloneCollection<T>(List<T> colItems) where T : IUniqueIdActiveRecord 
    { 
     List<T> objReturnCollection = new List<T>(); 
     //Clone the list - NEVER return the internal cache list 
     if (colItems != null && colItems.Count > 0) 
     { 
      List<T> colCachedItems = (List<T>)colItems; 
      foreach (T objItem in colCachedItems) 
      { 
       objReturnCollection.Add(objItem); 
      } 
     } 
     return objReturnCollection; 
    } 

    private static List<Guid> CloneCollection(List<Guid> colIds) 
    { 
     List<Guid> colReturnIds = new List<Guid>(); 
     //Clone the list - NEVER return the internal cache list 
     if (colIds != null && colIds.Count > 0) 
     { 
      List<Guid> colCachedItems = (List<Guid>)colIds; 
      foreach (Guid gId in colCachedItems) 
      { 
       colReturnIds.Add(gId); 
      } 
     } 
     return colReturnIds; 
    } 
    #endregion 

    #region Admin Functions 
    public static List<CollectionCacheEntry> GetAllCacheEntries() 
    { 
     return _htCollectionCache.Values.ToList(); 
    } 

    public static void ClearEntireCache() 
    { 
     _htCollectionCache.Clear(); 
    } 
    #endregion 

} 

public sealed class CollectionCacheEntry 
{ 
    public String Key; 
    public DateTime CacheEntry; 
    public DateTime LastUpdate; 
    public DateTime LastAccess; 
    public IList Collection; 
} 

Aquí es un ejemplo de cómo lo uso:

public static class ResourceCacheController 
{ 
    #region Cached Methods 
    public static List<Resource> GetResourcesByProject(Guid gProjectId) 
    { 
     String sKey = GetCacheKeyProjectResources(gProjectId); 
     List<Resource> colItems = CollectionCacheManager.FetchAndCache<Resource>(sKey, delegate() { return ResourceAccess.GetResourcesByProject(gProjectId); }); 
     return colItems; 
    } 

    #endregion 

    #region Cache Dependant Methods 
    public static int GetResourceCountByProject(Guid gProjectId) 
    { 
     return GetResourcesByProject(gProjectId).Count; 
    } 

    public static List<Resource> GetResourcesByIds(Guid gProjectId, List<Guid> colResourceIds) 
    { 
     if (colResourceIds == null || colResourceIds.Count == 0) 
     { 
      return null; 
     } 
     return GetResourcesByProject(gProjectId).FindAll(objRes => colResourceIds.Any(gId => objRes.Id == gId)).ToList(); 
    } 

    public static Resource GetResourceById(Guid gProjectId, Guid gResourceId) 
    { 
     return GetResourcesByProject(gProjectId).SingleOrDefault(o => o.Id == gResourceId); 
    } 
    #endregion 

    #region Cache Keys and Clear 
    public static void ClearCacheProjectResources(Guid gProjectId) 
    {   CollectionCacheManager.ClearCollection(GetCacheKeyProjectResources(gProjectId)); 
    } 

    public static string GetCacheKeyProjectResources(Guid gProjectId) 
    { 
     return string.Concat("ResourceCacheController.ProjectResources.", gProjectId.ToString()); 
    } 
    #endregion 

    internal static void ProcessDeleteResource(Guid gProjectId, Guid gResourceId) 
    { 
     Resource objRes = GetResourceById(gProjectId, gResourceId); 
     if (objRes != null) 
     {    CollectionCacheManager.RemoveItemFromCollection(GetCacheKeyProjectResources(gProjectId), objRes); 
     } 
    } 

    internal static void ProcessUpdateResource(Resource objResource) 
    { 
     CollectionCacheManager.UpdateItem(GetCacheKeyProjectResources(objResource.Id), objResource); 
    } 

    internal static void ProcessAddResource(Guid gProjectId, Resource objResource) 
    { 
     CollectionCacheManager.AddItemToCollection(GetCacheKeyProjectResources(gProjectId), objResource); 
    } 
} 

Esta es la unión en cuestión:

public interface IUniqueIdActiveRecord 
{ 
    Guid Id { get; set; } 

} 

Espero que esto ayude, yo he pasado por el infierno y volví un par de veces para finalmente llegar a esto como la solución, y para nosotros ha sido un godsen d, pero no puedo garantizar que sea perfecto, solo que no hemos encontrado un problema todavía.

+0

Esto realmente parece que resuelve el problema en cuestión, a pesar de ser (en mi opinión altamente sesgada) no tan resbaladizo como el que yo vine arriba con. ;) Definitivamente +1 para la implementación y creo que también cambiaré la respuesta aceptada a esta, ya que maneja la carga diferida que es tan fundamental para el problema. – Aaronaught

+0

Jaja, sí, tuve algunos más ingeniosos antes de esto, pero me encontré con algunos locos para resolver problemas de simultaneidad, finalmente reconstruí el tonto otra vez de la manera más a prueba de balas que pude. ¡Espero eso ayude! – JTtheGeek

3

Parece que las colecciones concurrentes de .NET 4.0 utilizan nuevas primitivas de sincronización que giran antes de cambiar de contexto, en caso de que un recurso se libere rápidamente. Entonces todavía están bloqueando, solo de una manera más oportunista. Si crees que la lógica de recuperación de datos es más corta que el ciclo de tiempo, parece que esto sería muy beneficioso. Pero mencionó la red, lo que me hace pensar que esto no se aplica.

Esperaría hasta tener una solución simple y sincronizada en su lugar, y medir el rendimiento y el comportamiento antes de asumir que tendrá problemas de rendimiento relacionados con la concurrencia.

Si realmente le preocupa la contención de caché, puede utilizar una infraestructura de caché existente y dividirla de manera lógica en regiones. Luego, sincronice el acceso a cada región de forma independiente.

Una estrategia de ejemplo si su conjunto de datos consiste en elementos codificados en ID numéricos, y si desea dividir su caché en 10 regiones, puede (mod 10) la ID para determinar en qué región se encuentran. d mantener una matriz de 10 objetos para bloquear. Todo el código se puede escribir para un número variable de regiones, que se pueden configurar a través de la configuración o determinadas al inicio de la aplicación, dependiendo de la cantidad total de elementos que prediga/tenga la intención de almacenar en caché.

Si los hits de la caché están codificados de forma anómala, deberá crear una heurística personalizada para particionar el caché.

Actualización (por comentario): Bueno, esto ha sido divertido. Creo que lo siguiente es un bloqueo tan preciso como puede esperar sin volverse totalmente loco (o mantener/sincronizar un diccionario de bloqueos para cada clave de caché). No lo he probado, por lo que probablemente haya errores, pero la idea debería ilustrarse. Haga un seguimiento de una lista de ID solicitados, y luego use eso para decidir si necesita obtener el artículo usted mismo, o si simplemente necesita esperar que termine una solicitud anterior. La espera (y la inserción de caché) se sincroniza con el bloqueo y la señalización de subprocesos de amplio alcance utilizando Wait y PulseAll.El acceso a la lista de ID solicitada se sincroniza con un ReaderWriterLockSlim de ámbito cerrado.

Esto es un caché de solo lectura. Si realiza creaciones/actualizaciones/eliminaciones, deberá asegurarse de eliminar los ID del requestedIds una vez que los haya recibido (antes de llamar a Monitor.PulseAll(_cache), querrá agregar otro try..finally y adquirir _requestedIdsLock write-lock). Además, con crea/actualiza/elimina, la forma más fácil de administrar la memoria caché sería simplemente eliminar el elemento existente de _cache si/cuando la operación de creación/actualización/eliminación subyacente tiene éxito.

(Vaya, ver actualización 2 a continuación.)

public class Item 
{ 
    public int ID { get; set; } 
} 

public class AsyncCache 
{ 
    protected static readonly Dictionary<int, Item> _externalDataStoreProxy = new Dictionary<int, Item>(); 

    protected static readonly Dictionary<int, Item> _cache = new Dictionary<int, Item>(); 

    protected static readonly HashSet<int> _requestedIds = new HashSet<int>(); 
    protected static readonly ReaderWriterLockSlim _requestedIdsLock = new ReaderWriterLockSlim(); 

    public Item Get(int id) 
    { 
     // if item does not exist in cache 
     if (!_cache.ContainsKey(id)) 
     { 
      _requestedIdsLock.EnterUpgradeableReadLock(); 
      try 
      { 
       // if item was already requested by another thread 
       if (_requestedIds.Contains(id)) 
       { 
        _requestedIdsLock.ExitUpgradeableReadLock(); 
        lock (_cache) 
        { 
         while (!_cache.ContainsKey(id)) 
          Monitor.Wait(_cache); 

         // once we get here, _cache has our item 
        } 
       } 
       // else, item has not yet been requested by a thread 
       else 
       { 
        _requestedIdsLock.EnterWriteLock(); 
        try 
        { 
         // record the current request 
         _requestedIds.Add(id); 
         _requestedIdsLock.ExitWriteLock(); 
         _requestedIdsLock.ExitUpgradeableReadLock(); 

         // get the data from the external resource 
         #region fake implementation - replace with real code 
         var item = _externalDataStoreProxy[id]; 
         Thread.Sleep(10000); 
         #endregion 

         lock (_cache) 
         { 
          _cache.Add(id, item); 
          Monitor.PulseAll(_cache); 
         } 
        } 
        finally 
        { 
         // let go of any held locks 
         if (_requestedIdsLock.IsWriteLockHeld) 
          _requestedIdsLock.ExitWriteLock(); 
        } 
       } 
      } 
      finally 
      { 
       // let go of any held locks 
       if (_requestedIdsLock.IsUpgradeableReadLockHeld) 
        _requestedIdsLock.ExitReadLock(); 
      } 
     } 

     return _cache[id]; 
    } 

    public Collection<Item> Get(Collection<int> ids) 
    { 
     var notInCache = ids.Except(_cache.Keys); 

     // if some items don't exist in cache 
     if (notInCache.Count() > 0) 
     { 
      _requestedIdsLock.EnterUpgradeableReadLock(); 
      try 
      { 
       var needToGet = notInCache.Except(_requestedIds); 

       // if any items have not yet been requested by other threads 
       if (needToGet.Count() > 0) 
       { 
        _requestedIdsLock.EnterWriteLock(); 
        try 
        { 
         // record the current request 
         foreach (var id in ids) 
          _requestedIds.Add(id); 

         _requestedIdsLock.ExitWriteLock(); 
         _requestedIdsLock.ExitUpgradeableReadLock(); 

         // get the data from the external resource 
         #region fake implementation - replace with real code 
         var data = new Collection<Item>(); 
         foreach (var id in needToGet) 
         { 
          var item = _externalDataStoreProxy[id]; 
          data.Add(item); 
         } 
         Thread.Sleep(10000); 
         #endregion 

         lock (_cache) 
         { 
          foreach (var item in data) 
           _cache.Add(item.ID, item); 

          Monitor.PulseAll(_cache); 
         } 
        } 
        finally 
        { 
         // let go of any held locks 
         if (_requestedIdsLock.IsWriteLockHeld) 
          _requestedIdsLock.ExitWriteLock(); 
        } 
       } 

       if (requestedIdsLock.IsUpgradeableReadLockHeld) 
        _requestedIdsLock.ExitUpgradeableReadLock(); 

       var waitingFor = notInCache.Except(needToGet); 
       // if any remaining items were already requested by other threads 
       if (waitingFor.Count() > 0) 
       { 
        lock (_cache) 
        { 
         while (waitingFor.Count() > 0) 
         { 
          Monitor.Wait(_cache); 
          waitingFor = waitingFor.Except(_cache.Keys); 
         } 

         // once we get here, _cache has all our items 
        } 
       } 
      } 
      finally 
      { 
       // let go of any held locks 
       if (_requestedIdsLock.IsUpgradeableReadLockHeld) 
        _requestedIdsLock.ExitReadLock(); 
      } 
     } 

     return new Collection<Item>(ids.Select(id => _cache[id]).ToList()); 
    } 
} 

Actualización 2:

no he entendido bien el comportamiento de UpgradeableReadLock ... sólo un hilo a la vez puede celebrar una UpgradeableReadLock . Por lo tanto, lo anterior se debe refactorizar para que solo capture los bloqueos de lectura inicialmente, y para que los abandone por completo y adquiera un bloqueo de escritura completo al agregar elementos al _requestedIds.

+0

Gracias, eso está muy cerca de la implementación que tenía en mente; El motivo por el que formulé la pregunta es que la implementación de mi propio caché también requiere que implemente espiraciones, compactación, persistencia, serialización de flujo y todas las otras miles de líneas de código que encontrará en una implementación de almacenamiento en caché de nivel de producción, que es lo que quiero evitar Definitivamente te daré +1 para la implementación; aún así, el problema más grande permanece sin resolver. – Aaronaught

+0

No terminé usando esta implementación, pero tuviste la mejor respuesta, ¡así que entiendes el punto! – Aaronaught

0

Finalmente se llegó a una solución viable para esto, gracias a un poco de diálogo en los comentarios. Lo que hice fue crear un contenedor, que es una clase base abstracta parcialmente implementada que usa cualquier biblioteca de caché estándar como la memoria caché de respaldo (solo necesita implementar los métodos Contains, Get, Put y Remove). Por el momento estoy usando el Bloque de aplicación de caché EntLib para eso, y me tomó un tiempo ponerlo en marcha porque algunos aspectos de esa biblioteca son ... bueno ... no muy bien pensados.

De todos modos, el código total es ahora de cerca de 1k líneas de modo que no voy a publicar toda la cosa aquí, pero la idea básica es:

  1. interceptar todas las llamadas a la Get, Put/Add, y Remove métodos.

  2. En lugar de añadir el elemento original, agregar un elemento "de entrada", que contiene una ManualResetEvent, además de una propiedad Value. Según algunos consejos que me han dado en una pregunta anterior hoy, la entrada implementa un bloqueo de cuenta atrás, que se incrementa cada vez que la entrada se adquiere y disminuye cada vez que se lanza. Tanto el cargador como todas las búsquedas futuras participan en el bloqueo de cuenta atrás, por lo que cuando el contador llega a cero, se garantiza que los datos estarán disponibles y se destruye el ManualResetEvent para conservar los recursos.

  3. Cuando una entrada tiene que cargarse de forma diferida, la entrada se crea y se agrega a la memoria caché de respaldo de inmediato, con el evento en un estado no asignado. Las llamadas posteriores ya sea al nuevo método GetOrAdd o a los métodos interceptados Get encontrarán esta entrada, y esperarán en el evento (si el evento existe) o devolverán el valor asociado inmediatamente (si el evento no existe).

  4. El método Put agrega una entrada sin eventos; estos tienen el mismo aspecto que las entradas para las cuales ya se completó la carga lenta.

  5. Debido a que el GetOrAdd todavía implementa un Get seguido por una opcional Put, este método está sincronizado (es serializado) contra los métodos Put y Remove, pero solamente para agregar la entrada incompleta, no para toda la duración de la perezoso carga. Los métodos Get son no serializados; efectivamente, toda la interfaz funciona como un bloqueo automático de lector-escritor.

Todavía es un trabajo en progreso, pero lo he ejecutado en una docena de pruebas unitarias y parece estar resistiendo. Se comporta correctamente para los dos escenarios descritos en la pregunta. En otras palabras:

  • Una llamada a larga lazy-carga (GetOrAdd) para la llave X (simulado por Thread.Sleep) que tarda 10 segundos, seguido por otro GetOrAdd para la misma clave X en un hilo diferente exactamente 9 segundos después, da como resultado que ambos hilos reciban los datos correctos al mismo tiempo (10 segundos desde T). Las cargas no están duplicadas.

  • cargar inmediatamente a un valor para la clave X, a continuación, iniciar un lazy-carga de larga duración para la clave Y, a continuación, solicitando clave X en otro hilo (antes Y es terminado), da inmediatamente respalde el valor de X. Las llamadas de bloqueo están aisladas a la clave relevante.

También da lo que creo que es el resultado más intuitivo para cuando se inicia un lento carga e inmediatamente eliminar la clave de la memoria caché; el hilo que originalmente solicitó el valor obtendrá el valor real, pero cualquier otro hilo que solicite la misma clave en cualquier momento después de la eliminación no obtendrá nada de nuevo (null) y devolverá inmediatamente.

En general estoy bastante contento con eso. Todavía me gustaría que hubiera una biblioteca que hiciera esto por mí, pero supongo que si quieres hacer algo bien ... bueno, ya sabes.

+0

Genial, sería genial verlo si pudieras encontrar el tiempo/energía/recursos para publicarlo en alguna parte. –

+0

@gWiz: ¿Dónde la gente normalmente aloja este tipo de cosas? Normalmente no hago todo el código abierto, aunque esto se ha convertido en una biblioteca bastante útil.La verdadera magia está en la biblioteca de interceptación de métodos, que estoy usando para decorar métodos con 'CacheAttribute' y hacer que automáticamente asigne ranuras de caché basadas en los argumentos. Me complace ponerlo en algún lado, simplemente no tengo experiencia con la distribución de fuentes. : P – Aaronaught

+0

Esa es una buena pregunta. Si desea configurarlo para tener una infraestructura de proyecto completamente alojada, ciertamente hay codeplex.com o sourceforge.net. Creo que ambos usan svn y requieren un poco de trabajo para configurarlo. Pero si solo desea alojar el archivo en alguna parte como referencia (y si no tiene un blog) simplemente puede cargarlo en Google Docs (y cambiar los permisos de uso compartido para que los usuarios no necesiten iniciar sesión para verlo). –

1

Implementé una biblioteca simple llamada MemoryCacheT. Está en GitHub y NuGet. Básicamente almacena elementos en un ConcurrentDictionary y puede especificar la estrategia de caducidad al agregar artículos. Cualquier comentario, revisión, sugerencia es bienvenido.

Cuestiones relacionadas