Pour commencer, laissez-moi simplement dire que je sais que le code ci-dessous n'est pas thread-safe (correction: peut-être). Ce avec quoi j'ai du mal, c'est de trouver une implémentation qui soit et que je puisse réellement réussir à échouer lors des tests. Je suis en train de refactoriser un grand projet WCF qui nécessite des données (principalement) statiques mises en cache et alimentées à partir d'une base de données SQL. Il doit expirer et "se rafraîchir" au moins une fois par jour, c'est pourquoi j'utilise MemoryCache.
Je sais que le code ci-dessous ne doit pas être thread-safe, mais je ne peux pas le faire échouer sous une charge lourde et pour compliquer les choses, une recherche Google montre les implémentations dans les deux sens (avec et sans verrous combinés à des débats sur la nécessité ou non de les utiliser.
Une personne connaissant MemoryCache dans un environnement multi-thread pourrait-elle me faire savoir définitivement si je dois ou non verrouiller le cas échéant afin qu'un appel à supprimer (qui sera rarement appelé mais c'est une exigence) ne sera pas lancé lors de la récupération / repeuplement.
public class MemoryCacheService : IMemoryCacheService
{
private const string PunctuationMapCacheKey = "punctuationMaps";
private static readonly ObjectCache Cache;
private readonly IAdoNet _adoNet;
static MemoryCacheService()
{
Cache = MemoryCache.Default;
}
public MemoryCacheService(IAdoNet adoNet)
{
_adoNet = adoNet;
}
public void ClearPunctuationMaps()
{
Cache.Remove(PunctuationMapCacheKey);
}
public IEnumerable GetPunctuationMaps()
{
if (Cache.Contains(PunctuationMapCacheKey))
{
return (IEnumerable) Cache.Get(PunctuationMapCacheKey);
}
var punctuationMaps = GetPunctuationMappings();
if (punctuationMaps == null)
{
throw new ApplicationException("Unable to retrieve punctuation mappings from the database.");
}
if (punctuationMaps.Cast<IPunctuationMapDto>().Any(p => p.UntaggedValue == null || p.TaggedValue == null))
{
throw new ApplicationException("Null values detected in Untagged or Tagged punctuation mappings.");
}
// Store data in the cache
var cacheItemPolicy = new CacheItemPolicy
{
AbsoluteExpiration = DateTime.Now.AddDays(1.0)
};
Cache.AddOrGetExisting(PunctuationMapCacheKey, punctuationMaps, cacheItemPolicy);
return punctuationMaps;
}
//Go oldschool ADO.NET to break the dependency on the entity framework and need to inject the database handler to populate cache
private IEnumerable GetPunctuationMappings()
{
var table = _adoNet.ExecuteSelectCommand("SELECT [id], [TaggedValue],[UntaggedValue] FROM [dbo].[PunctuationMapper]", CommandType.Text);
if (table != null && table.Rows.Count != 0)
{
return AutoMapper.Mapper.DynamicMap<IDataReader, IEnumerable<PunctuationMapDto>>(table.CreateDataReader());
}
return null;
}
}
la source
Réponses:
Le MS par défaut fourni
MemoryCache
est entièrement thread-safe. Toute implémentation personnalisée qui dérive deMemoryCache
peut ne pas être thread-safe. Si vous utilisez plainMemoryCache
out of the box, il est thread-safe. Parcourez le code source de ma solution de mise en cache distribuée open source pour voir comment je l'utilise (MemCache.cs):https://github.com/haneytron/dache/blob/master/Dache.CacheHost/Storage/MemCache.cs
la source
GetOrCreate
méthode. il y a un problème à ce sujet sur githubBien que MemoryCache soit en effet thread-safe comme d'autres réponses l'ont spécifié, il a un problème de multi-threads commun - si 2 threads essaient
Get
de (ou vérifientContains
) le cache en même temps, alors les deux manqueront le cache et les deux finiront par générer le résultat et les deux ajouteront alors le résultat au cache.Cela n'est souvent pas souhaitable - le deuxième thread doit attendre que le premier se termine et utiliser son résultat plutôt que de générer des résultats deux fois.
C'est l'une des raisons pour lesquelles j'ai écrit LazyCache - un wrapper convivial sur MemoryCache qui résout ce genre de problèmes. Il est également disponible sur Nuget .
la source
AddOrGetExisting
, au lieu d'implémenter une logique personnalisée autourContains
. LaAddOrGetExisting
méthode de MemoryCache est atomique et threadsafe referencesource.microsoft.com/System.Runtime.Caching/R/...Comme d'autres l'ont indiqué, MemoryCache est en effet thread-safe. Cependant, la sécurité des threads des données qui y sont stockées dépend entièrement de votre utilisation.
Pour citer Reed Copsey de son impressionnante poste au sujet de concomitance et le
ConcurrentDictionary<TKey, TValue>
genre. Ce qui est bien sûr applicable ici.Vous pouvez imaginer que ce serait particulièrement mauvais si la
TValue
construction est coûteuse.Pour contourner ce
Lazy<T>
problème , vous pouvez tirer parti très facilement, ce qui, par coïncidence, est très bon marché à construire. Cela garantit que si nous nous trouvons dans une situation multithread, nous ne construisons que plusieurs instances deLazy<T>
(ce qui est bon marché).GetOrAdd()
(GetOrCreate()
dans le cas deMemoryCache
) retournera le même, singulierLazy<T>
à tous les threads, les instances "supplémentaires" deLazy<T>
sont simplement jetées.Puisque le
Lazy<T>
ne fait rien tant qu'il.Value
n'est pas appelé, une seule instance de l'objet est construite.Maintenant pour un peu de code! Vous trouverez ci-dessous une méthode d'extension pour
IMemoryCache
laquelle implémente ce qui précède. Il est arbitrairement définiSlidingExpiration
sur la base d'un paramètre deint seconds
méthode. Mais cela est entièrement personnalisable en fonction de vos besoins.public static T GetOrAdd<T>(this IMemoryCache cache, string key, int seconds, Func<T> factory) { return cache.GetOrCreate<T>(key, entry => new Lazy<T>(() => { entry.SlidingExpiration = TimeSpan.FromSeconds(seconds); return factory.Invoke(); }).Value); }
Appeler:
IMemoryCache cache; var result = cache.GetOrAdd("someKey", 60, () => new object());
Pour effectuer tout cela de manière asynchrone, je recommande d'utiliser l' excellente
AsyncLazy<T>
implémentation de Stephen Toub trouvée dans son article sur MSDN. Qui combine l'initialiseur paresseux intégréLazy<T>
avec la promesseTask<T>
:public class AsyncLazy<T> : Lazy<Task<T>> { public AsyncLazy(Func<T> valueFactory) : base(() => Task.Factory.StartNew(valueFactory)) { } public AsyncLazy(Func<Task<T>> taskFactory) : base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap()) { } }
Maintenant, la version asynchrone de
GetOrAdd()
:public static Task<T> GetOrAddAsync<T>(this IMemoryCache cache, string key, int seconds, Func<Task<T>> taskFactory) { return cache.GetOrCreateAsync<T>(key, async entry => await new AsyncLazy<T>(async () => { entry.SlidingExpiration = TimeSpan.FromSeconds(seconds); return await taskFactory.Invoke(); }).Value); }
Et enfin, pour appeler:
IMemoryCache cache; var result = await cache.GetOrAddAsync("someKey", 60, async () => new object());
la source
MemoryCache.GetOrCreate
est pas sûre de la même manièreConcurrentDictionary
estLazy
? Si oui, comment avez-vous validé cela?GetOrCreate
utiliser la même clé et cette usine. le résultat, l'usine a été utilisée 10 fois lors de l'utilisation avec le cache mémoire (vu les impressions) + à chaque fois que leGetOrCreate
retourne une valeur différente! J'ai exécuté le même test en utilisantConcurrentDicionary
et j'ai vu l'usine être utilisée une seule fois, et j'ai toujours la même valeur. J'ai trouvé un problème fermé dessus dans github, je viens d'écrire un commentaire qu'il devrait être rouvertConsultez ce lien: http://msdn.microsoft.com/en-us/library/system.runtime.caching.memorycache(v=vs.110).aspx
Allez tout en bas de la page (ou recherchez le texte «Thread Safety»).
Tu verras:
la source
MemoryCache.Default
à très haut volume (des millions de hits de cache par minute) sans aucun problème de threading pour le moment.Je viens de télécharger un exemple de bibliothèque pour résoudre le problème de .Net 2.0.
Jetez un œil sur ce repo:
RedisLazyCache
J'utilise le cache Redis mais il bascule également ou simplement Memorycache si Connectionstring est manquant.
Il est basé sur la bibliothèque LazyCache qui garantit une exécution unique du rappel pour l'écriture en cas de multi-threading essayant de charger et d'enregistrer des données, spécialement si le rappel est très coûteux à exécuter.
la source
Comme mentionné par @AmitE à la réponse de @pimbrouwers, son exemple ne fonctionne pas comme démontré ici:
class Program { static async Task Main(string[] args) { var cache = new MemoryCache(new MemoryCacheOptions()); var tasks = new List<Task>(); var counter = 0; for (int i = 0; i < 10; i++) { var loc = i; tasks.Add(Task.Run(() => { var x = GetOrAdd(cache, "test", TimeSpan.FromMinutes(1), () => Interlocked.Increment(ref counter)); Console.WriteLine($"Interation {loc} got {x}"); })); } await Task.WhenAll(tasks); Console.WriteLine("Total value creations: " + counter); Console.ReadKey(); } public static T GetOrAdd<T>(IMemoryCache cache, string key, TimeSpan expiration, Func<T> valueFactory) { return cache.GetOrCreate(key, entry => { entry.SetSlidingExpiration(expiration); return new Lazy<T>(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication); }).Value; } }
Production:
Interation 6 got 8 Interation 7 got 6 Interation 2 got 3 Interation 3 got 2 Interation 4 got 10 Interation 8 got 9 Interation 5 got 4 Interation 9 got 1 Interation 1 got 5 Interation 0 got 7 Total value creations: 10
Il semble que
GetOrCreate
renvoie toujours l'entrée créée. Heureusement, c'est très facile à réparer:public static T GetOrSetValueSafe<T>(IMemoryCache cache, string key, TimeSpan expiration, Func<T> valueFactory) { if (cache.TryGetValue(key, out Lazy<T> cachedValue)) return cachedValue.Value; cache.GetOrCreate(key, entry => { entry.SetSlidingExpiration(expiration); return new Lazy<T>(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication); }); return cache.Get<Lazy<T>>(key).Value; }
Cela fonctionne comme prévu:
Interation 4 got 1 Interation 9 got 1 Interation 1 got 1 Interation 8 got 1 Interation 0 got 1 Interation 6 got 1 Interation 7 got 1 Interation 2 got 1 Interation 5 got 1 Interation 3 got 1 Total value creations: 1
la source
Le cache est threadsafe, mais comme d'autres l'ont indiqué, il est possible que GetOrAdd appelle la fonction plusieurs types si l'appel à partir de plusieurs types.
Voici ma solution minimale à ce sujet
private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1);
et
await _cacheLock.WaitAsync(); var data = await _cache.GetOrCreateAsync(key, entry => ...); _cacheLock.Release();
la source