Pregunta Bibliotecas de caché Thread-safe para .NET


Fondo:

Mantengo varias aplicaciones de Winforms y bibliotecas de clases que pueden o ya se benefician del almacenamiento en caché. También estoy al tanto de Caching Application Block y el System.Web.Caching namespace (que, por lo que he reunido, está perfectamente bien para usar fuera de ASP.NET).

Descubrí 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 método similar al que está en el nuevo ConcurrentDictionary clase en .NET 4.0.

Considero que tal método es un primitivo para la funcionalidad de caché / búsqueda, y obviamente los diseñadores del 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 una aplicación de "cliente enriquecido" (o incluso algunas aplicaciones web) es comenzar a precargar un caché tan pronto como se inicie la aplicación, bloqueando si el cliente solicita datos que aún no se han cargado (posteriormente guardándolo en caché para el futuro utilizar). 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.

Así que parece que me quedan algunas opciones igualmente pésimas:

  • No intente hacer la operación atómica en absoluto, y arriesgue que los datos se carguen dos veces (y posiblemente tengan dos hilos diferentes operando en diferentes copias);

  • Serializar el acceso al caché, lo que significa bloquear el caché completo solo para cargar un objeto unico;

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


Aclaración: Ejemplo de línea de tiempo

Digamos que cuando se inicia una aplicación, necesita cargar 3 conjuntos de datos que cada uno tarda 10 segundos en cargar. Considere las siguientes dos líneas de tiempo:

00:00 - Comienza a cargar el Dataset 1
00:10 - Comienza a cargar el Dataset 2
00:19 - El usuario pide el Dataset 2

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

00:00 - Comienza a cargar el Dataset 1
00:10 - Comienza a cargar el Dataset 2
00:11 - El usuario pide el Dataset 1

En este caso, el usuario está solicitando datos que ya están en el caché Pero si serializamos el acceso al caché, tendrá que esperar otros 9 segundos sin ningún motivo, porque el administrador de caché (sea lo que sea) no tiene conocimiento de la artículo específico pidiéndolo, solo ese "algo" está siendo solicitado y "algo" está en progreso.


La pregunta:

¿Hay alguna biblioteca de almacenamiento en caché para .NET (pre-4.0) que hacer implementar tales operaciones atómicas, como cabría esperar de un caché seguro para subprocesos?

O, alternativamente, ¿hay algún medio para extender un caché "thread-safe" existente para soportar tales operaciones, sin serializar el acceso a la memoria caché (lo que en primer lugar vencería el propósito de usar una implementación segura para subprocesos)? Dudo que exista, pero tal vez estoy cansado e ignorando una solución obvia.

O ... ¿hay algo más que me pierdo? ¿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?


31
2018-02-24 22:50


origen


Respuestas:


Conozco 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í está 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í hay 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);
    }
}

Aquí está la interfaz en cuestión:

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

}

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


5
2017-12-07 23:37



Parece que las colecciones simultáneas 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 que tuviese una solución simple y sincronizada y mediría el desempeño y el comportamiento antes de asumir que tendrá problemas de rendimiento relacionados con la concurrencia.

Si realmente le preocupa la contención del 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 que están codificados en ID numéricos, y desea dividir su caché en 10 regiones, puede (mod 10) la ID para determinar en qué región se encuentran. Usted mantendría una matriz de 10 objetos para enganchar. 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 accesos directos a la caché están codificados de forma anormal, deberá crear una heurística personalizada para particionar el caché.

Actualizar (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 la memoria caché) se sincroniza con el bloqueo y la señalización del hilo con un amplio margen Wait y PulseAll. El acceso a la lista de ID solicitada se sincroniza con un ámbito estrictoReaderWriterLockSlim.

Esta es una caché de solo lectura. Si crea / actualiza / elimina, deberá asegurarse de eliminar los ID de requestedIds una vez que se reciben (antes de la llamada a Monitor.PulseAll(_cache) querrás agregar otro try..finally y adquiere el _requestedIdsLock bloqueo de escritura). 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.

(Ups, ver actualización 2 abajo.)

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 entendí el comportamiento de UpgradeableReadLock ... solo un hilo a la vez puede contener un ActualizableReadLock. Por lo tanto, lo anterior se debe refactorizar para que solo capture inicialmente los bloqueos de lectura, y para que los abandone por completo y adquiera un bloqueo de escritura completo cuando agregue elementos a _requestedIds.


3
2018-02-24 23:41



Implementé una biblioteca simple llamada MemoryCacheT. Esta encendido 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.


1
2017-09-23 19:48



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 el Contains, Get, Puty Remove métodos). 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 ahora está cerca de 1k líneas, así que no voy a publicar todo aquí, pero la idea básica es:

  1. Interceptar todas las llamadas al Get, Put/Addy Remove métodos.

  2. En lugar de agregar el elemento original, agregue un elemento de "entrada" que contiene un ManualResetEvent además de un Value propiedad. 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 el ManualResetEvent se destruye para conservar recursos.

  3. Cuando una entrada tiene que cargarse de forma diferida, la entrada se crea y agrega al caché de respaldo de inmediato, con el evento en un estado no asignado. Llamadas posteriores a la nueva GetOrAdd método o el interceptado Get los métodos 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. los Put el método agrega una entrada sin evento; estos tienen el mismo aspecto que las entradas para las cuales ya se completó la carga lenta.

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

Todavía es un trabajo en progreso, pero lo he pasado por 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 la carga lenta de larga duración (GetOrAdd) para la llave X (simulado por Thread.Sleep) que toma 10 segundos, seguido por otro GetOrAdd por la misma clave X en un hilo diferente exactamente 9 segundos después, los resultados en ambos hilos reciben los datos correctos al mismo tiempo (10 segundos desde T0) Las cargas no están duplicadas.

  • Inmediatamente cargando un valor para la clave X, luego inicia una carga lenta de larga duración para la clave Y, luego solicitando la clave X en otro hilo (antes Y finaliza), inmediatamente devuelve 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 comienzas una carga diferida y luego inmediatamente quitas la clave del 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 regresa de inmediato.

En general estoy bastante feliz 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
2018-02-26 00:14