Sicherheit von MemoryCache-Threads, ist Sperren erforderlich?

Sicherheit von MemoryCache-Threads, ist Sperren erforderlich?

Der standardmäßig von MS bereitgestellte MemoryCache ist absolut threadsicher. Jede benutzerdefinierte Implementierung, die von MemoryCache abgeleitet ist möglicherweise nicht threadsicher. Wenn Sie einfach MemoryCache verwenden Out of the Box ist es threadsicher. Durchsuchen Sie den Quellcode meiner verteilten Open-Source-Caching-Lösung, um zu sehen, wie ich sie verwende (MemCache.cs):

https://github.com/haneytron/dache/blob/master/Dache.CacheHost/Storage/MemCache.cs


Während MemoryCache tatsächlich Thread-sicher ist, wie andere Antworten angegeben haben, hat es ein häufiges Multi-Threading-Problem - wenn 2 Threads versuchen, Get von (oder überprüfen Sie Contains ) den Cache zur gleichen Zeit, dann werden beide den Cache verfehlen und beide werden am Ende das Ergebnis generieren und beide werden dann das Ergebnis zum Cache hinzufügen.

Dies ist oft unerwünscht – der zweite Thread sollte warten, bis der erste fertig ist, und sein Ergebnis verwenden, anstatt Ergebnisse zweimal zu generieren.

Das war einer der Gründe, warum ich LazyCache geschrieben habe – einen freundlichen Wrapper für MemoryCache, der diese Art von Problemen löst. Es ist auch auf Nuget verfügbar.


Wie andere bereits gesagt haben, ist MemoryCache tatsächlich Thread-sicher. Die Thread-Sicherheit der darin gespeicherten Daten hängt jedoch vollständig von Ihrer Verwendung ab.

Um Reed Copsey aus seinem großartigen Beitrag zur Parallelität und dem ConcurrentDictionary<TKey, TValue> zu zitieren Typ. Was hier natürlich zutrifft.

Sie können sich vorstellen, dass dies besonders schlimm wäre, wenn TValue ist teuer zu bauen.

Um dies zu umgehen, können Sie Lazy<T> nutzen sehr leicht, was übrigens sehr billig zu konstruieren ist. Dadurch wird sichergestellt, dass wir, wenn wir in eine Multithread-Situation geraten, nur mehrere Instanzen von Lazy<T> erstellen (was billig ist).

GetOrAdd() (GetOrCreate() im Fall von MemoryCache ) gibt denselben singulären Lazy<T> zurück für alle Threads die "zusätzlichen" Instanzen von Lazy<T> werden einfach weggeworfen.

Seit Lazy<T> tut nichts bis .Value aufgerufen wird, wird immer nur eine Instanz des Objekts erstellt.

Jetzt für etwas Code! Unten ist eine Erweiterungsmethode für IMemoryCache die das obige implementiert. Es setzt willkürlich SlidingExpiration basierend auf einem int seconds Methodenparam. Dies ist jedoch vollständig an Ihre Bedürfnisse anpassbar.

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

Anrufen:

IMemoryCache cache;
var result = cache.GetOrAdd("someKey", 60, () => new object());

Um dies alles asynchron durchzuführen, empfehle ich die Verwendung von Stephen Toubs hervorragendem AsyncLazy<T> Implementierung in seinem Artikel auf MSDN gefunden. Das kombiniert den eingebauten faulen Initialisierer Lazy<T> mit dem Versprechen Task<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())
    { }
}   

Jetzt die asynchrone Version von 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);
}

Und schließlich zum Aufruf:

IMemoryCache cache;
var result = await cache.GetOrAddAsync("someKey", 60, async () => new object());