web-dev-qa-db-fra.com

Modèle de verrouillage pour une utilisation correcte de .NET MemoryCache

Je suppose que ce code a des problèmes de simultanéité:

const string CacheKey = "CacheKey";
static string GetCachedData()
{
    string expensiveString =null;
    if (MemoryCache.Default.Contains(CacheKey))
    {
        expensiveString = MemoryCache.Default[CacheKey] as string;
    }
    else
    {
        CacheItemPolicy cip = new CacheItemPolicy()
        {
            AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
        };
        expensiveString = SomeHeavyAndExpensiveCalculation();
        MemoryCache.Default.Set(CacheKey, expensiveString, cip);
    }
    return expensiveString;
}

La raison du problème de concurrence est que plusieurs threads peuvent obtenir une clé NULL puis tenter d’insérer des données dans le cache.

Quel serait le moyen le plus court et le plus propre de rendre ce code résistant à la concurrence? J'aime suivre un bon schéma dans mon code associé au cache. Un lien vers un article en ligne serait d'une grande aide.

MISE À JOUR:

Je suis arrivé avec ce code basé sur la réponse de @Scott Chamberlain. Quelqu'un peut-il trouver un problème de performances ou de concurrence avec cela? Si cela fonctionne, cela économisera beaucoup de lignes de code et d’erreurs.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Caching;

namespace CachePoc
{
    class Program
    {
        static object everoneUseThisLockObject4CacheXYZ = new object();
        const string CacheXYZ = "CacheXYZ";
        static object everoneUseThisLockObject4CacheABC = new object();
        const string CacheABC = "CacheABC";

        static void Main(string[] args)
        {
            string xyzData = MemoryCacheHelper.GetCachedData<string>(CacheXYZ, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
            string abcData = MemoryCacheHelper.GetCachedData<string>(CacheABC, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
        }

        private static string SomeHeavyAndExpensiveXYZCalculation() {return "Expensive";}
        private static string SomeHeavyAndExpensiveABCCalculation() {return "Expensive";}

        public static class MemoryCacheHelper
        {
            public static T GetCachedData<T>(string cacheKey, object cacheLock, int cacheTimePolicyMinutes, Func<T> GetData)
                where T : class
            {
                //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
                T cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                if (cachedData != null)
                {
                    return cachedData;
                }

                lock (cacheLock)
                {
                    //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
                    cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                    if (cachedData != null)
                    {
                        return cachedData;
                    }

                    //The value still did not exist so we now write it in to the cache.
                    CacheItemPolicy cip = new CacheItemPolicy()
                    {
                        AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(cacheTimePolicyMinutes))
                    };
                    cachedData = GetData();
                    MemoryCache.Default.Set(cacheKey, cachedData, cip);
                    return cachedData;
                }
            }
        }
    }
}
103
Allan Xu

Ceci est ma 2ème itération du code. Étant donné que MemoryCache est thread-safe, vous n'avez pas besoin de verrouiller la lecture initiale. Vous pouvez simplement lire et si le cache renvoie null, effectuez la vérification du verrouillage pour voir si vous devez créer la chaîne. Cela simplifie grandement le code.

_const string CacheKey = "CacheKey";
static readonly object cacheLock = new object();
private static string GetCachedData()
{

    //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
    var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

    if (cachedString != null)
    {
        return cachedString;
    }

    lock (cacheLock)
    {
        //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
        cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }

        //The value still did not exist so we now write it in to the cache.
        var expensiveString = SomeHeavyAndExpensiveCalculation();
        CacheItemPolicy cip = new CacheItemPolicy()
                              {
                                  AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
                              };
        MemoryCache.Default.Set(CacheKey, expensiveString, cip);
        return expensiveString;
    }
}
_

EDIT: Le code ci-dessous est inutile mais je voulais le laisser pour montrer la méthode originale. Cela peut être utile aux futurs visiteurs qui utilisent une collection différente avec des lectures sécurisées pour le fil, mais des écritures non sécurisées (la quasi-totalité des classes sous l'espace de noms _System.Collections_ est comme ça).

Voici comment je le ferais en utilisant ReaderWriterLockSlim pour protéger l’accès. Vous devez faire une sorte de " Verrouillage vérifié " pour voir si quelqu'un d'autre a créé l'élément mis en cache pendant que nous attendions de prendre le verrou.

_const string CacheKey = "CacheKey";
static readonly ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
static string GetCachedData()
{
    //First we do a read lock to see if it already exists, this allows multiple readers at the same time.
    cacheLock.EnterReadLock();
    try
    {
        //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
        var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }
    }
    finally
    {
        cacheLock.ExitReadLock();
    }

    //Only one UpgradeableReadLock can exist at one time, but it can co-exist with many ReadLocks
    cacheLock.EnterUpgradeableReadLock();
    try
    {
        //We need to check again to see if the string was created while we where waiting to enter the EnterUpgradeableReadLock
        var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }

        //The entry still does not exist so we need to create it and enter the write lock
        var expensiveString = SomeHeavyAndExpensiveCalculation();
        cacheLock.EnterWriteLock(); //This will block till all the Readers flush.
        try
        {
            CacheItemPolicy cip = new CacheItemPolicy()
            {
                AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
            };
            MemoryCache.Default.Set(CacheKey, expensiveString, cip);
            return expensiveString;
        }
        finally 
        {
            cacheLock.ExitWriteLock();
        }
    }
    finally
    {
        cacheLock.ExitUpgradeableReadLock();
    }
}
_
82
Scott Chamberlain

Il existe une bibliothèque open source [disclaimer: que j'ai écrit]: LazyCache qu'OMI couvre votre besoin avec deux lignes de code:

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

Il a par défaut un verrouillage intégré pour que la méthode pouvant être mise en cache ne s'exécute qu'une fois par erreur de cache et utilise un lambda pour que vous puissiez "obtenir ou ajouter" en une fois. La valeur par défaut est de 20 minutes d’expiration glissante.

Il y a même n paquet NuGet ;)

38
alastairtree

J'ai résolu ce problème en utilisant la méthode AddOrGetExisting sur MemoryCache et l'utilisation de initialisation différée .

Essentiellement, mon code ressemble à ceci:

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

Dans le pire des cas, vous créez deux fois le même objet Lazy. Mais c'est assez trivial. L'utilisation de AddOrGetExisting garantit que vous ne recevrez jamais qu'une seule instance de l'objet Lazy, et vous ne pourrez donc appeler qu'une fois la méthode d'initialisation coûteuse.

31
Keith

Je suppose que ce code a des problèmes de simultanéité:

En fait, c'est peut-être bien, mais avec une amélioration possible.

Maintenant, en général, le modèle où plusieurs threads définissent une valeur partagée lors de la première utilisation, pour ne pas verrouiller la valeur obtenue et définie, peut être:

  1. Désastreux - un autre code supposera qu'une seule instance existe.
  2. Désastreux - le code qui obtient l'instance ne peut tolérer qu'une (ou peut-être un certain nombre) d'opérations simultanées.
  3. Désastreux - le moyen de stockage n'est pas thread-safe (par exemple, ajouter deux dictionnaires à un dictionnaire et obtenir toutes sortes d'erreurs désagréables).
  4. Sous-optimal - les performances globales sont pires que si le verrouillage avait assuré qu'un seul thread effectuait le travail d'obtention de la valeur.
  5. Optimal - le coût de la duplication de tâches multiples par un travail redondant est inférieur au coût de sa prévention, d'autant plus que cela ne peut se produire que pendant une période relativement brève.

Cependant, considérant ici que MemoryCache peut expulser des entrées alors:

  1. S'il est désastreux d'avoir plus d'une instance, alors MemoryCache n'est pas la bonne approche.
  2. Si vous devez empêcher la création simultanée, vous devez le faire au moment de la création.
  3. MemoryCache est thread-safe en termes d'accès à cet objet, donc ce n'est pas un problème ici.

Bien entendu, ces deux possibilités doivent être envisagées, bien que le seul cas où deux instances de la même chaîne existent peut poser problème, si vous effectuez des optimisations très particulières qui ne s'appliquent pas ici *.

Nous avons donc les possibilités suivantes:

  1. Il est moins coûteux d’éviter les appels en double à SomeHeavyAndExpensiveCalculation().
  2. Il est moins coûteux de ne pas éviter le coût des appels en double vers SomeHeavyAndExpensiveCalculation().

Et travailler avec ça peut être difficile (en effet, le genre de chose où il vaut la peine de faire un profil plutôt que de supposer que vous pouvez le résoudre). Il convient toutefois de noter ici que les moyens les plus évidents de verrouiller une insertion empêchent les ajouts tous au cache, y compris ceux qui ne sont pas liés.

Cela signifie que si nous avions 50 threads essayant de définir 50 valeurs différentes, nous devrons alors les faire attendre les uns les autres, même s'ils ne feraient même pas le même calcul.

En tant que tel, vous êtes probablement mieux avec le code que vous avez, qu'avec un code qui évite la condition de concurrence, et si la condition de concurrence est un problème, vous aurez probablement besoin de le gérer ailleurs ou d'avoir besoin d'un autre stratégie de mise en cache par rapport à celle qui supprime les anciennes entrées †.

La seule chose que je changerais est que je remplacerais l'appel à Set() par un à AddOrGetExisting(). D'après ce qui précède, il devrait être clair que ce n'est probablement pas nécessaire, mais cela permettrait de récupérer l'élément nouvellement obtenu, réduisant ainsi l'utilisation globale de la mémoire et permettant un ratio plus élevé de collections de faible à haute génération.

Alors oui, vous pouvez utiliser le double verrouillage pour empêcher la concurrence, mais soit la concurrence n'est pas réellement un problème, soit vous ne stockez pas les valeurs correctement, ou le double verrouillage sur le magasin ne serait pas le meilleur moyen de le résoudre. .

* Si vous savez qu'il n'existe qu'une seule chaîne dans un jeu de chaînes, vous pouvez optimiser les comparaisons d'égalité, ce qui est à peu près le seul moment où deux copies d'une chaîne peuvent être incorrectes plutôt que simplement sous-optimales, mais vous voudriez le faire. types de mise en cache très différents pour que cela ait un sens. Par exemple. la sorte XmlReader fait en interne.

† Très probablement soit un magasin qui stocke indéfiniment, soit un lecteur qui utilise des références faibles afin qu'il n'expulsera les entrées que s'il n'y a pas d'utilisations existantes.

15
Jon Hanna
public interface ILazyCacheProvider : IAppCache
{
    /// <summary>
    /// Get data loaded - after allways throw cached result (even when data is older then needed) but very fast!
    /// </summary>
    /// <param name="key"></param>
    /// <param name="getData"></param>
    /// <param name="slidingExpiration"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    T GetOrAddPermanent<T>(string key, Func<T> getData, TimeSpan slidingExpiration);
}

/// <summary>
/// Initialize LazyCache in runtime
/// </summary>
public class LazzyCacheProvider: CachingService, ILazyCacheProvider
{
    private readonly Logger _logger = LogManager.GetLogger("MemCashe");
    private readonly Hashtable _hash = new Hashtable();
    private readonly List<string>  _reloader = new List<string>();
    private readonly ConcurrentDictionary<string, DateTime> _lastLoad = new ConcurrentDictionary<string, DateTime>();  


    T ILazyCacheProvider.GetOrAddPermanent<T>(string dataKey, Func<T> getData, TimeSpan slidingExpiration)
    {
        var currentPrincipal = Thread.CurrentPrincipal;
        if (!ObjectCache.Contains(dataKey) && !_hash.Contains(dataKey))
        {
            _hash[dataKey] = null;
            _logger.Debug($"{dataKey} - first start");
            _lastLoad[dataKey] = DateTime.Now;
            _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
            _lastLoad[dataKey] = DateTime.Now;
           _logger.Debug($"{dataKey} - first");
        }
        else
        {
            if ((!ObjectCache.Contains(dataKey) || _lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) < DateTime.Now) && _hash[dataKey] != null)
                Task.Run(() =>
                {
                    if (_reloader.Contains(dataKey)) return;
                    lock (_reloader)
                    {
                        if (ObjectCache.Contains(dataKey))
                        {
                            if(_lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) > DateTime.Now)
                                return;
                            _lastLoad[dataKey] = DateTime.Now;
                            Remove(dataKey);
                        }
                        _reloader.Add(dataKey);
                        Thread.CurrentPrincipal = currentPrincipal;
                        _logger.Debug($"{dataKey} - reload start");
                        _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
                        _logger.Debug($"{dataKey} - reload");
                        _reloader.Remove(dataKey);
                    }
                });
        }
        if (_hash[dataKey] != null) return (T) (_hash[dataKey]);

        _logger.Debug($"{dataKey} - dummy start");
        var data = GetOrAdd(dataKey, getData, slidingExpiration);
        _logger.Debug($"{dataKey} - dummy");
        return (T)((object)data).CloneObject();
    }
}
1
art24war

Exemple de console de MemoryCache , "Comment enregistrer/obtenir des objets de classe simples"

Sortie après avoir lancé et appuyé sur Any key sauf Esc :

Sauvegarde en cache!
Sortir de la cache!
Some1
Some2

    class Some
    {
        public String text { get; set; }

        public Some(String text)
        {
            this.text = text;
        }

        public override string ToString()
        {
            return text;
        }
    }

    public static MemoryCache cache = new MemoryCache("cache");

    public static string cache_name = "mycache";

    static void Main(string[] args)
    {

        Some some1 = new Some("some1");
        Some some2 = new Some("some2");

        List<Some> list = new List<Some>();
        list.Add(some1);
        list.Add(some2);

        do {

            if (cache.Contains(cache_name))
            {
                Console.WriteLine("Getting from cache!");
                List<Some> list_c = cache.Get(cache_name) as List<Some>;
                foreach (Some s in list_c) Console.WriteLine(s);
            }
            else
            {
                Console.WriteLine("Saving to cache!");
                cache.Set(cache_name, list, DateTime.Now.AddMinutes(10));                   
            }

        } while (Console.ReadKey(true).Key != ConsoleKey.Escape);

    }
1
fr0ga