31 votes

Bibliothèques de cache thread-safe pour .NET

Arrière-plan:

Je maintiens plusieurs applications Winforms et les bibliothèques de classes pouvant ou déjà faire bénéficier de la mise en cache. Je suis également au courant de la mise en Cache de Bloc d'Application et le Système.Web.La mise en cache de noms (qui, d'après ce que j'ai recueillies, est parfaitement OK pour utilisation à l'extérieur ASP.NET).

J'ai trouvé que, bien que les deux classes ci-dessus sont techniquement "thread-safe" dans le sens que les méthodes individuelles sont synchronisés, ils n'ont pas vraiment l'air d'être conçu particulièrement pour le multi-thread scénarios. Plus précisément, ils ne sont pas en œuvre un GetOrAdd méthode similaire à celle de la nouvelle - ConcurrentDictionary classe .NET 4.0.

Nous pensons qu'une telle méthode soit une primitive de cache et de la fonctionnalité de recherche, et, évidemment, le Cadre designers, conscience de ce trop - c'est pourquoi les méthodes existent dans la concurrente de collections. Cependant, hormis le fait que je ne suis pas à l'aide .NET 4.0 dans la production d'applications encore, un dictionnaire n'est pas un véritable cache - elle n'a pas de caractéristiques comme des expirations, persistant/stockage distribué, etc.


Pourquoi c'est important:

Un assez typique de la conception dans un "client riche" de l'app (ou même des applications web) est de commencer pré-chargement d'un cache dès que l'application démarre, le blocage si le client demande des données qui n'est pas encore chargé (suite de la mise en cache pour une utilisation ultérieure). Si l'utilisateur est de labourer par le biais de son flux de travail rapidement, ou si la connexion réseau est lente, il n'est pas rare du tout pour le client d'être en concurrence avec le preloader, et il n'a vraiment pas beaucoup de sens de demander deux fois les mêmes données, en particulier si la demande est relativement cher.

Alors il me semble d'être de gauche avec un peu de tout aussi moche options:

  • N'essayez pas de faire l'opération atomique à tous, et le risque de les données en cours de chargement deux fois (et peut-être avoir deux threads différents exploitation sur différentes copies);

  • Sérialiser l'accès à la mémoire cache, ce qui signifie que le verrouillage de l' intégralité du cache pour charger un seul élément;

  • Début de réinventer la roue, juste pour obtenir un supplément de quelques méthodes.


Précisions: Exemple De Montage

Dire que lorsque l'application démarre, il doit charger 3 les ensembles de données qui chaque de prendre 10 secondes pour charger. Réfléchissez à ces deux échéances:

00:00 - Commencer le chargement Dataset 1
00:10 - Démarrer le chargement Dataset 2
00:19 - Utilisateur en fait la demande Dataset 2

Dans le cas ci-dessus, si nous n'utilisons pas de tout type de synchronisation, l'utilisateur doit attendre 10 secondes pour les données qui seront disponibles dans 1 seconde, parce que le code va voir que l'article n'est pas encore chargé dans le cache et essayez de la recharger.

00:00 - Commencer le chargement Dataset 1
00:10 - Démarrer le chargement Dataset 2
00:11 - l'Utilisateur demande de l'ensemble de données 1

Dans ce cas, l'utilisateur est en demandant des données qui est déjà dans le cache. Mais si nous sérialiser l'accès à la cache, il va falloir attendre encore 9 secondes pour aucune raison du tout, parce que le gestionnaire de cache (quelle qu'elle soit) n'a pas de prise de conscience de l' élément spécifique demandé, seulement que "quelque chose" est demandée et de "quelque chose" est en cours.


La Question:

Existe-il des bibliothèques de mise en cache pour .NET (pré-4.0) qui n' mettre en œuvre de telles opérations atomiques, que l'on peut attendre d'un thread-safe cache?

Ou sinon, est-il un moyen de prolonger l'existant "thread-safe" cache à l'appui de telles opérations, sans la sérialisation des accès à la mémoire cache (ce qui irait à l'encontre de l'objectif de l'utilisation d'un thread-safe mise en œuvre en premier lieu)? Je doute qu'il y est, mais peut-être que je suis juste fatigué et en ignorant une solution évidente.

Ou... est-il autre chose que je suis absent? Est-il juste de pratique courante de laisser deux concurrents fils steamroll les uns des autres si elles se produisent à la fois demander le même article, dans le même temps, pour la première fois ou après une date d'expiration?

6voto

JTtheGeek Points 1149

Je sais que votre douleur comme je suis l'un des Architectes de Dedoose. J'ai foiré autour avec beaucoup de bibliothèques de mise en cache et a terminé la construction de ce l'un après beaucoup de tribulations. Une hypothèse pour ce Gestionnaire de Cache, c'est que toutes les collections conservées par cette classe implémente une interface pour obtenir un Guid comme une propriété "Id" sur chaque objet. C'est pour une RIA il comprend beaucoup de méthodes pour ajouter /mise à jour /suppression d'éléments de ces collections.

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

Voici un exemple de comment je l'utilise:

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

Voici l'Interface en question:

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

}

Espérons que cela aide, j'ai vécu l'enfer et le dos un peu de temps pour enfin arriver à ce que la solution, et pour nous, C'est une véritable aubaine, mais je ne peux pas garantir que c'est parfait, la seule que nous n'avons pas trouvé encore un problème.

3voto

G-Wiz Points 4800

Il ressemble à l' .NET 4.0 simultanées collections utilisent de nouvelles primitives de synchronisation que le spin avant de changer de contexte, dans le cas où une ressource est libérée rapidement. Donc ils sont toujours de verrouillage, mais seulement d'une façon plus opportuniste. Si vous pensez à la récupération de données logique est plus courte que la timeslice, alors il semble que ce serait très bénéfique. Mais vous avez mentionné réseau, ce qui me fait penser que cela ne s'applique pas.

Je voudrais attendre jusqu'à ce que vous avez un simple, synchronisé solution en place, et de mesurer la performance et le comportement avant d'en supposant que vous avez des problèmes de performance liés à la concurrence.

Si vous êtes vraiment préoccupés par le cache de contention, vous pouvez utiliser un cache existant de l'infrastructure et logiquement partition en régions. Puis synchroniser l'accès à chaque région de manière indépendante.

Un exemple de stratégie si votre jeu de données se compose des éléments qui sont à la clé sur des Id numériques, et vous souhaitez partitionner votre cache dans 10 régions, vous pouvez (mod 10) ID de déterminer la région où ils se trouvent. Vous auriez du garder un tableau de 10 objets de verrouillage. Tout le code peut être écrit pour un nombre variable de régions, qui peut être défini à l'aide de la configuration, ou déterminée en application de démarrage en fonction du nombre total d'articles vous prédire/l'intention de cache.

Si votre cache sont conçus d'une façon anormale, vous aurez à venir avec quelques personnalisé heuristique de la partition du cache.

Mise à jour (par commentaire): Eh bien cela a été amusant. Je pense que la suite est aussi fine-grain de verrouillage que vous pouvez espérer sans aller complètement fou (ou le maintien/la synchronisation d'un dictionnaire de serrures pour chaque clé de cache). Je ne l'ai pas testé donc il y a probablement des bugs, mais l'idée doit être illustrée. Un suivi de l'Id, et ensuite l'utiliser pour décider si vous avez besoin de l'élément de vous-même, ou si vous avez simplement besoin d'attendre une demande antérieure à la fin. D'attente (et de cache d'insertion) est synchronisé avec très étendue de blocage du thread et de signalisation à l'aide de Wait et PulseAll. Accès à la liste des ID est synchronisé avec un très étendueReaderWriterLockSlim.

C'est un cache en lecture seule. Si tu fais crée, mises à jour et des suppressions, vous devrez assurez-vous de vous enlever les Id de requestedIds une fois qu'ils sont reçus (avant l'appel à l' Monitor.PulseAll(_cache) vous aurez envie d'ajouter un autre try..finally et d'acquérir de l' _requestedIdsLock écrivez-lock). Aussi, avec crée/mises à jour/suppression, la façon la plus simple de gérer le cache serait de simplement supprimer l'élément existant à partir d' _cache si/lorsque le sous-jacent créer/mettre à jour/supprimer l'opération réussit.

(Oups, voir mise à jour 2 ci-dessous).

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

Mise à jour 2:

J'ai mal compris le comportement de UpgradeableReadLock... un seul thread à la fois peut tenir une UpgradeableReadLock. Donc, le dessus doit être reconstruit pour seulement saisir les verrous en Lecture d'abord, et de complètement céder et acquérir un véritable verrou en Écriture lors de l'ajout d'éléments d' _requestedIds.

2voto

Ufuk Hacıoğulları Points 16499

J'ai implémenté une bibliothèque simple appelée MemoryCacheT. C'est sur GitHub et NuGet . Il stocke essentiellement des éléments dans un ConcurrentDictionary et vous pouvez spécifier une stratégie d'expiration lors de l'ajout d'éléments. Tous les commentaires, critiques, suggestions sont les bienvenus.

0voto

Aaronaught Points 73049

Enfin est venu avec une solution viable à ce, grâce à un dialogue dans les commentaires. Ce que j'ai fait était de créer un wrapper, qui est partiellement mis en œuvre abstraite de la classe de base qui utilise une norme de cache de la bibliothèque comme support de cache (juste besoin de mettre en œuvre l' Contains, Get, Put, et Remove méthodes). Pour le moment je suis en utilisant le EntLib la mise en Cache de Bloc d'Application de cette, et il a fallu un certain temps pour obtenir cette place et en cours d'exécution parce que certains aspects de cette bibliothèque sont... eh bien... pas bien pensé.

De toute façon, le code est maintenant proche de 1k lignes donc je ne vais pas poster la totalité de la chose ici, mais l'idée de base est:

  1. Intercepter tous les appels à l' Get, Put/Add, et Remove méthodes.

  2. Au lieu d'ajouter l'élément d'origine, ajouter une "entrée" de l'élément qui contient un ManualResetEvent en plus pour un Value de la propriété. Selon certains conseils donnés à moi sur une question précédente, aujourd'hui, l'entrée implémente un compte à rebours loquet, qui est incrémenté chaque fois que l'entrée est acquis et décrémenté à chaque fois qu'il est libéré. À la fois le chargeur et toutes les futures recherches de participer dans le compte à rebours loquet, de sorte que lorsque le compteur atteint zéro, les données sont garantis d'être disponibles et de l' ManualResetEvent est détruite afin de préserver les ressources.

  3. Quand une entrée est à chargement paresseux, l'entrée est créé et ajouté à la sauvegarde du cache d'emblée, avec l'événement dans un unsignaled état. Les appels suivants à la nouvelle GetOrAdd méthode ou la intercepté Get méthodes trouverez cette entrée, et soit attendre l'événement (si l'événement n'existe) ou le retour de la valeur associée immédiatement (si l'événement n'existe pas).

  4. L' Put méthode ajoute une entrée avec aucun événement; ces le même aspect que les entrées pour lesquelles chargement différé a déjà été réalisé.

  5. Parce que l' GetOrAdd encore implémente un Get suivie par une option Put, cette méthode est synchronisé (sérialisés) contre l' Put et Remove méthodes, mais seulement pour ajouter l'entrée incomplète, pas pour toute la durée de la charge paresseux. L' Get méthodes sont pas sérialisé; efficace de l'ensemble de l'interface fonctionne comme un lecteur automatique-verrou en écriture.

C'est toujours un travail en cours, mais j'ai couru à travers une douzaine de tests unitaires et il semble tenir le coup. Il se comporte correctement pour les deux scénarios décrits dans la question. En d'autres termes:

  • Un appel à longue lazy-load (GetOrAdd) pour la touche X (simulé par Thread.Sleep), ce qui prend 10 secondes, suivi par un autre GetOrAdd pour la même touche X sur un thread différent exactement 9 secondes plus tard, les résultats dans les deux fils de la réception correcte des données en même temps (10 secondes à partir de T0). Les charges ne sont pas dupliqués.

  • Immédiatement le chargement d'une valeur de touche X, puis à partir d'une longue lazy-load pour la touche Y, puis demander la clé de X sur un autre thread (avant Y est fini), donne immédiatement la valeur de X. Blocage des appels sont isolés à la clé correspondante.

Il donne également ce que je pense est le plus intuitif résultat lorsque vous commencez un lazy-load, puis retirez immédiatement la clé du cache; le fil qui avait demandé à l'origine de la valeur d'obtenir la valeur réelle, mais les autres threads qui demande la même clé, en tout temps après la suppression d'obtenir rien en retour (null) et de revenir immédiatement.

Dans l'ensemble je suis très heureux avec elle. Je souhaite toujours il y avait une bibliothèque qui l'as fait pour moi, mais je suppose que, si vous voulez que quelque chose soit bien fait... bien, vous savez.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X