Je suppose que ce code a des problèmes de concurrence:
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;
}
Le problème de concurrence est dû au fait que plusieurs threads peuvent obtenir une clé nulle, 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 à l'épreuve de la concurrence? J'aime suivre un bon modèle dans mon code lié au cache. Un lien vers un article en ligne serait d'une grande aide.
METTRE À JOUR:
J'ai trouvé ce code basé sur la réponse de @Scott Chamberlain. Quelqu'un peut-il trouver un problème de performance ou de concurrence avec cela? Si cela fonctionne, cela sauverait de nombreuses 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;
}
}
}
}
}
c#
.net
multithreading
memorycache
Allan Xu
la source
la source
ReaderWriterLockSlim
?ReaderWriterLockSlim
... Mais j'utiliserais aussi cette technique pour éviter lestry-finally
déclarations.Dictionary<string, object>
où la clé est la même clé que vous utilisez dans votreMemoryCache
et l'objet dans le dictionnaire est juste un élément de baseObject
sur lequel vous verrouillez. Cependant, cela étant dit, je vous recommanderais de lire la réponse de Jon Hanna. Sans un profilage approprié, vous risquez de ralentir davantage votre programme avec le verrouillage que de louer deux instances d'SomeHeavyAndExpensiveCalculation()
exécution et de perdre un résultat.Réponses:
C'est ma 2ème itération du code. Comme il
MemoryCache
est thread-safe, vous n'avez pas besoin de verrouiller sur la lecture initiale, vous pouvez simplement lire et si le cache retourne null, effectuez la vérification du verrouillage pour voir si vous devez créer la chaîne. Cela simplifie grandement le code.EDIT : Le code ci-dessous n'est pas nécessaire mais je voulais le laisser pour montrer la méthode d'origine. Cela peut être utile pour les futurs visiteurs qui utilisent une collection différente qui a des lectures thread-safe mais des écritures non thread-safe (presque toutes les classes sous l'
System.Collections
espace de noms sont comme ça).Voici comment je le ferais en utilisant
ReaderWriterLockSlim
pour protéger l'accès. Vous devez faire une sorte de " Verrouillage à double vérification " pour voir si quelqu'un d'autre a créé l'élément mis en cache pendant que nous attendions pour prendre le verrou.la source
MemoryCache
il est probable qu'au moins une de ces deux choses était erronée.Il existe une bibliothèque open source [avertissement: que j'ai écrit]: LazyCache que l'OMI couvre vos besoins avec deux lignes de code:
Il a intégré le verrouillage par défaut de sorte que la méthode pouvant être mise en cache ne s'exécutera qu'une seule fois par manque de cache, et elle utilise un lambda afin que vous puissiez faire "obtenir ou ajouter" en une seule fois. La valeur par défaut est de 20 minutes d'expiration glissante.
Il y a même un package NuGet ;)
la source
J'ai résolu ce problème en utilisant la méthode AddOrGetExisting sur le MemoryCache et l'utilisation de l' initialisation Lazy .
Essentiellement, mon code ressemble à ceci:
Le pire des cas ici est que vous créez le même
Lazy
objet deux fois. Mais c'est assez trivial. L'utilisation deAddOrGetExisting
garanties que vous n'obtiendrez jamais qu'une seule instance de l'Lazy
objet, et ainsi vous êtes également assuré de n'appeler qu'une seule fois la méthode d'initialisation coûteuse.la source
SomeHeavyAndExpensiveCalculationThatResultsAString()
une exception a été lancée, elle est bloquée dans le cache. Même les exceptions transitoires seront mises en cache avecLazy<T>
: msdn.microsoft.com/en-us/library/vstudio/dd642331.aspxEn fait, c'est très probablement bien, mais avec une amélioration possible.
Maintenant, en général, le modèle où nous avons plusieurs threads définissant une valeur partagée lors de la première utilisation, pour ne pas verrouiller sur la valeur obtenue et définie peut être:
Cependant, considérant ici que
MemoryCache
peut expulser les entrées alors:MemoryCache
c'est la mauvaise approche.MemoryCache
est thread-safe en termes d'accès à cet objet, ce n'est donc pas un problème ici.Bien entendu, il faut réfléchir à ces deux possibilités, bien que le seul moment où deux instances de la même chaîne existent peut être un problème si vous faites des optimisations très particulières qui ne s'appliquent pas ici *.
Donc, il nous reste les possibilités:
SomeHeavyAndExpensiveCalculation()
.SomeHeavyAndExpensiveCalculation()
.Et travailler sur cela peut être difficile (en effet, le genre de chose où il vaut la peine de profiler plutôt que de supposer que vous pouvez le résoudre). Cela vaut la peine de considérer ici que les moyens les plus évidents de verrouillage sur l'insert empêcheront tout ajouts 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, alors nous devrons faire attendre les 50 threads les uns sur les autres, même s'ils n'allaient même pas faire le même calcul.
En tant que tel, vous êtes probablement mieux avec le code que vous avez, qu'avec du code qui évite la condition de concurrence, et si la condition de concurrence est un problème, vous devrez probablement gérer cela ailleurs, ou avoir besoin d'un autre une stratégie de mise en cache qui expulse les anciennes entrées †
La seule chose que je changerais, c'est que je remplacerais l'appel à
Set()
par un autreAddOrGetExisting()
. D'après ce qui précède, il devrait être clair que ce n'est probablement pas nécessaire, mais cela permettrait à l'élément nouvellement obtenu d'être collecté, réduisant l'utilisation globale de la mémoire et permettant un rapport plus élevé entre les collections de faible génération et de haute génération.Donc 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 votre stockage des valeurs dans le mauvais sens, soit le double verrouillage sur le magasin ne serait pas le meilleur moyen de le résoudre .
* Si vous ne connaissez qu'une seule de chaque ensemble de chaînes existe, 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 des types de mise en cache très différents pour que cela ait du sens. Par exemple, le tri
XmlReader
fait en interne.† Très probablement soit celui qui stocke indéfiniment, soit celui qui utilise des références faibles, de sorte qu'il n'expulsera les entrées que s'il n'y a pas d'utilisations existantes.
la source
Pour éviter le verrou global, vous pouvez utiliser SingletonCache pour implémenter un verrou par clé, sans exploser l'utilisation de la mémoire (les objets de verrouillage sont supprimés lorsqu'ils ne sont plus référencés, et l'acquisition / la libération est thread-safe garantissant qu'une seule instance est toujours utilisée via comparer et swap).
Son utilisation ressemble à ceci:
Le code est ici sur GitHub: https://github.com/bitfaster/BitFaster.Caching
Il existe également une implémentation LRU qui est plus légère que MemoryCache et présente plusieurs avantages - lectures et écritures simultanées plus rapides, taille limitée, pas de thread d'arrière-plan, compteurs de performances internes, etc. (avertissement, je l'ai écrit).
la source
Exemple de console de MemoryCache , "Comment enregistrer / récupérer des objets de classe simples"
Sortie après lancement et appui Any keysaufEsc :
Enregistrer dans le cache!
Récupérer du cache!
Some1
Some2
la source
la source
C'est un peu tard, cependant ... Mise en œuvre complète:
Voici la
getPageContent
signature:Et voici le
MemoryCacheWithPolicy
mise en œuvre:nlogger
est juste unnLog
objet pour tracer leMemoryCacheWithPolicy
comportement. Je recrée le cache mémoire si l'objet de requête (RequestQuery requestQuery
) est modifié via le délégué (Func<TParameter, TResult> createCacheData
) ou je le recrée lorsque le temps de glissement ou absolu atteint sa limite. Notez que tout est aussi asynchrone;)la source
Il est difficile de choisir lequel est le meilleur; lock ou ReaderWriterLockSlim. Vous avez besoin de statistiques du monde réel sur les nombres et les ratios de lecture et d'écriture, etc.
Mais si vous pensez que l'utilisation de "lock" est la bonne manière. Alors voici une solution différente pour différents besoins. J'inclus également la solution d'Allan Xu dans le code. Parce que les deux peuvent être nécessaires pour des besoins différents.
Voici les exigences qui me poussent vers cette solution:
Code:
la source