62 votes

Comment gérer les opérations de construction coûteuses utilisant MemoryCache?

Sur un ASP.NET projet MVC nous avons plusieurs cas de données qui nécessite une bonne quantité de temps et de ressources pour construire. Nous voulons les mettre en cache.

MemoryCache fournit un certain niveau de fil de sécurité, mais pas assez pour éviter l'exécution de plusieurs instances de code du bâtiment en parallèle. Voici un exemple:

var data = cache["key"];
if(data == null)
{
  data = buildDataUsingGoodAmountOfResources();
  cache["key"] = data;
}

Comme vous pouvez le voir sur un site occupé de centaines de threads peuvent aller à l'intérieur de la si la déclaration simultanément jusqu'à ce que les données sont construites et de faire de l'exploitation d'un bâtiment encore plus lente, consommer inutilement les ressources du serveur.

Il y a un atomiques AddOrGetExisting mise en œuvre dans MemoryCache mais il requiert incorrectement "valeur pour définir" au lieu de "code pour récupérer la valeur à définir" qui, je pense, rend la méthode donnée presque totalement inutile.

Nous avons été à l'aide de notre propre ad-hoc de l'échafaudage autour de MemoryCache pour y arriver mais il faut explicite locks. C'est gênant à l'utilisation par l'entrée de verrouillage des objets et nous avons l'habitude de sortir par le partage d'objets de verrou qui est loin d'être idéale. Qui m'a fait penser que les raisons d'éviter une telle convention pourrait être intentionnelle.

J'ai donc deux questions:

  • Est-il préférable de ne pas lock code du bâtiment? (Qui peut avoir été prouvé plus réactifs pour l'un, je me demande)

  • Quel est le bon chemin pour atteindre par l'entrée de verrouillage pour MemoryCache pour une telle serrure? La forte envie d'utiliser key chaîne de caractères comme le verrou de l'objet est rejeté à l' ".NET de blocage 101".

73voto

ssg Points 20321

Nous avons résolu ce problème en combinant Lazy<T> avec AddOrGetExisting pour éviter de recourir complètement à un objet verrou. Voici un exemple de code (qui utilise une expiration infinie):

 public T GetFromCache<T>(string key, Func<T> valueFactory) 
{
    var newValue = new Lazy<T>(valueFactory);
    // the line belows returns existing item or adds the new value if it doesn't exist
    var value = (Lazy<T>)cache.AddOrGetExisting(key, newValue, MemoryCache.InfiniteExpiration);
    return (value ?? newValue).Value; // Lazy<T> handles the locking itself
}
 

Ce n'est pas complet. Il y a des pièges comme "la mise en cache des exceptions", vous devez donc décider de ce que vous voulez faire dans le cas où votre valueFactory lève une exception. Un des avantages, cependant, est la possibilité de mettre en cache aussi les valeurs nulles.

11voto

Andras Zoltan Points 24996

Pour le conditionnel ajouter exigence, j'ai toujours utiliser ConcurrentDictionary, ce qui a surchargé GetOrAdd méthode qui accepte un délégué à feu si l'objet doit être construit.

ConcurrentDictionary<string, object> _cache = new
  ConcurrenctDictionary<string, object>();

public void GetOrAdd(string key)
{
  return _cache.GetOrAdd(key, (k) => {
    //here 'k' is actually the same as 'key'
    return buildDataUsingGoodAmountOfResources();
  });
}

En réalité, j'ai presque toujours utiliser static simultanées dictionnaires. J'ai utilisé "normal" dictionnaires protégé par un ReaderWriterLockSlim de l'instance, mais dès que je suis passé à .Net 4 (il est uniquement disponible à partir de ce partir) j'ai commencé la conversion de l'un de ceux que j'ai croisé.

ConcurrentDictionarys'performance est admirable pour dire le moins :)

Mise à jour Naïf de mise en œuvre de l'expiration de la sémantique fondée sur l'âge uniquement. Doivent également veiller à ce que les éléments individuels sont créés uniquement pour une fois - comme par @usr suggestion. Mise à jour - comme @usr a suggéré simplement à l'aide d'un Lazy<T> serait beaucoup plus simple - vous pouvez juste avant la création de délégué pour que lors de l'ajout à la concurrente de dictionnaire. Je be changé le code, comme le fait mon dictionnaire de serrures n'aurait pas marché de toute façon. Mais faut vraiment que j'y ai pensé moi-même (au-delà de minuit, ici, dans le royaume-UNI et si je suis battu. Toute la sympathie? Sans évidemment pas. Étant développeur, j'ai assez de caféine qui coule dans mes veines à réveiller les morts).

Je ne le recommande la mise en œuvre de l' IRegisteredObject interface avec la présente, bien que, et puis de l'enregistrer avec l' HostingEnvironment.RegisterObject méthode - faire, qui constituerait un moyen le plus propre pour arrêter le poller fil lorsque le pool d'applications s'éteint/recycle.

public class ConcurrentCache : IDisposable
{
  private readonly ConcurrentDictionary<string, Tuple<DateTime?, Lazy<object>>> _cache = 
    new ConcurrentDictionary<string, Tuple<DateTime?, Lazy<object>>>();

  private readonly Thread ExpireThread = new Thread(ExpireMonitor);

  public ConcurrentCache(){
    ExpireThread.Start();
  }

  public void Dispose()
  {
    //yeah, nasty, but this is a 'naive' implementation :)
    ExpireThread.Abort();
  }

  public void ExpireMonitor()
  {
    while(true)
    {
      Thread.Sleep(1000);
      DateTime expireTime = DateTime.Now;
      var toExpire = _cache.Where(kvp => kvp.First != null &&
        kvp.Item1.Value < expireTime).Select(kvp => kvp.Key).ToArray();
      Tuple<string, Lazy<object>> removed;
      object removedLock;
      foreach(var key in toExpire)
      {
        _cache.TryRemove(key, out removed);
      }
    }
  }

  public object CacheOrAdd(string key, Func<string, object> factory, 
    TimeSpan? expiry)
  {
    return _cache.GetOrAdd(key, (k) => { 
      //get or create a new object instance to use 
      //as the lock for the user code
        //here 'k' is actually the same as 'key' 
        return Tuple.Create(
          expiry.HasValue ? DateTime.Now + expiry.Value : (DateTime?)null,
          new Lazy<object>(() => factory(k)));
    }).Item2.Value; 
  }
}

1voto

Neil Whitaker Points 2886

Voici une conception qui suit ce que vous semblez avoir à l’esprit. Le premier verrou ne survient que pour une courte période. Le dernier appel à data.Value est également verrouillé (en dessous), mais les clients ne le bloquent que si deux d'entre eux demandent le même élément au même moment.

 public DataType GetData()
{      
  lock(_privateLockingField)
  {
    Lazy<DataType> data = cache["key"] as Lazy<DataType>;
    if(data == null)
    {
      data = new Lazy<DataType>(() => buildDataUsingGoodAmountOfResources();
      cache["key"] = data;
    }
  }

  return data.Value;
}
 

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