MemoryCache-Thread-Sicherheit, ist eine Sperrung erforderlich?

87

Lassen Sie mich zunächst einmal wissen, dass der folgende Code nicht threadsicher ist (Korrektur: möglicherweise). Ich habe Probleme damit, eine Implementierung zu finden, die tatsächlich getestet werden kann. Ich überarbeite gerade ein großes WCF-Projekt, für das einige (meistens) statische Daten zwischengespeichert und aus einer SQL-Datenbank ausgefüllt werden müssen. Es muss mindestens einmal am Tag ablaufen und "aktualisiert" werden, weshalb ich MemoryCache verwende.

Ich weiß, dass der folgende Code nicht threadsicher sein sollte, aber ich kann nicht erreichen, dass er unter hoher Last fehlschlägt und um die Sache zu komplizieren. Eine Google-Suche zeigt Implementierungen in beide Richtungen (mit und ohne Sperren kombiniert mit Debatten, ob sie notwendig sind oder nicht).

Könnte jemand mit Kenntnissen über MemoryCache in einer Umgebung mit mehreren Threads mich definitiv wissen lassen, ob ich gegebenenfalls sperren muss, damit ein zu entfernender Aufruf (der selten aufgerufen wird, aber erforderlich ist) beim Abrufen / erneuten Auffüllen nicht ausgelöst wird.

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;
    }
}
James Legan
quelle
ObjectCache ist threadsicher. Ich glaube nicht, dass Ihre Klasse fehlschlagen kann. msdn.microsoft.com/en-us/library/… Möglicherweise rufen Sie gleichzeitig die Datenbank auf, dies verbraucht jedoch nur mehr CPU als erforderlich.
the_lotus
1
ObjectCache ist zwar threadsicher, die Implementierungen jedoch möglicherweise nicht. Also die MemoryCache Frage.
Haney

Antworten:

76

Die standardmäßige MS-Bereitstellung MemoryCacheist vollständig threadsicher. Eine benutzerdefinierte Implementierung, von der abgeleitet wird, ist MemoryCachemöglicherweise nicht threadsicher. Wenn Sie " MemoryCacheout of the box" verwenden, ist es threadsicher. Durchsuchen Sie den Quellcode meiner Open Source Distributed Caching-Lösung, um zu sehen, wie ich sie verwende (MemCache.cs):

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

Haney
quelle
2
David, Nur um zu bestätigen, dass in der sehr einfachen Beispielklasse, die ich oben habe, der Aufruf von .Remove () tatsächlich threadsicher ist, wenn ein anderer Thread gerade Get () aufruft? Ich nehme an, ich sollte nur den Reflektor verwenden und tiefer graben, aber es gibt viele widersprüchliche Informationen.
James Legan
12
Es ist threadsicher, aber anfällig für Rennbedingungen ... Wenn Ihr Get vor dem Entfernen erfolgt, werden die Daten auf dem Get zurückgegeben. Wenn das Entfernen zuerst erfolgt, wird dies nicht der Fall sein. Dies ähnelt stark schmutzigen Lesevorgängen in einer Datenbank.
Haney
9
Erwähnenswert (wie ich in einer anderen Antwort unten kommentiert habe), ist die Dotnet-Core- Implementierung derzeit NICHT vollständig threadsicher. speziell die GetOrCreateMethode. Es gibt ein Problem auf Github
AmitE
Löst der Speichercache beim Ausführen von Threads über die Konsole eine Ausnahme "Nicht genügend Speicher" aus?
Faseeh Haris
47

Während MemoryCache in der Tat threadsicher ist, wie andere Antworten angegeben haben, gibt es ein häufiges Problem mit mehreren Threads. Wenn zwei Threads gleichzeitig versuchen, den Cache zu verlassen Get(oder zu überprüfen Contains), verfehlen beide den Cache und beide generieren am Ende Das Ergebnis und beide fügen das Ergebnis dann dem Cache hinzu.

Oft ist dies unerwünscht - der zweite Thread sollte warten, bis der erste abgeschlossen ist, und sein Ergebnis verwenden, anstatt zweimal Ergebnisse zu generieren.

Dies war einer der Gründe, warum ich LazyCache geschrieben habe - einen benutzerfreundlichen Wrapper für MemoryCache, der diese Art von Problemen löst. Es ist auch auf Nuget erhältlich .

Alastairtree
quelle
15

Wie bereits erwähnt, ist MemoryCache in der Tat threadsicher. Die Thread-Sicherheit der darin gespeicherten Daten hängt jedoch ganz von Ihrer Verwendung ab.

Um Reed Copsey aus seinem großartigen Beitrag über Parallelität und den ConcurrentDictionary<TKey, TValue>Typ zu zitieren . Welches ist natürlich hier anwendbar.

Wenn zwei Threads dies gleichzeitig [GetOrAdd] aufrufen, können problemlos zwei Instanzen von TValue erstellt werden.

Sie können sich vorstellen, dass dies besonders schlimm wäre, wenn TValueder Bau teuer wäre.

Um dies zu umgehen, können Sie es Lazy<T>sehr einfach nutzen, was zufällig sehr billig zu konstruieren ist. Auf diese Weise stellen wir sicher, dass wir nur mehrere Instanzen von Lazy<T>(was billig ist) erstellen, wenn wir in eine Multithread-Situation geraten .

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

Da das Lazy<T>nichts tut, bis .Valuees aufgerufen wird, wird immer nur eine Instanz des Objekts erstellt.

Nun zu etwas Code! Unten finden Sie eine Erweiterungsmethode, für IMemoryCachedie die oben genannten implementiert werden. Die Einstellung SlidingExpirationbasiert willkürlich auf einem int secondsMethodenparameter. Dies ist jedoch vollständig an Ihre Bedürfnisse anpassbar.

Beachten Sie, dass dies spezifisch für .netcore2.0-Apps ist

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 der hervorragenden AsyncLazy<T>Implementierung von Stephen Toub, die in seinem Artikel über MSDN zu finden ist. Was den eingebauten Lazy Initialisierer Lazy<T>mit dem Versprechen kombiniert 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 zum Schluss:

IMemoryCache cache;
var result = await cache.GetOrAddAsync("someKey", 60, async () => new object());
Zuhälter
quelle
2
Ich habe es versucht und es scheint nicht zu funktionieren (Dot Net Core 2.0). Jedes GetOrCreate erstellt eine neue Lazy-Instanz, und der Cache wird mit dem neuen Lazy aktualisiert. Daher wird der Wert mehrmals ausgewertet (erstellt) (in einer Umgebung mit mehreren Threads).
AmitE
2
von den Blicken von ihm, 2,0 .netcore MemoryCache.GetOrCreatewird auf die gleiche Art und Weise nicht Thread sicher ConcurrentDictionaryist
amite
Wahrscheinlich eine dumme Frage, aber sind Sie sicher, dass der Wert mehrfach erstellt wurde und nicht der Lazy? Wenn ja, wie haben Sie dies validiert?
Pimbrouwers
3
Ich habe eine Factory-Funktion verwendet, die auf dem Bildschirm gedruckt wird, wenn sie verwendet wurde + eine Zufallszahl generiert und 10 Threads gestartet hat, die alle versuchen, GetOrCreatedenselben Schlüssel und diese Factory zu verwenden. Das Ergebnis war, dass die Factory 10 Mal verwendet wurde, wenn sie mit dem Speichercache verwendet wurde (sah die Ausdrucke) + jedes Mal, wenn GetOrCreateein anderer Wert zurückgegeben wurde! Ich habe den gleichen Test mit durchgeführt ConcurrentDicionaryund gesehen, dass die Fabrik nur einmal benutzt wurde, und habe immer den gleichen Wert erhalten. Ich habe ein geschlossenes Problem in Github gefunden. Ich habe dort gerade einen Kommentar geschrieben, dass es wieder geöffnet werden soll
AmitE
Warnung: Diese Antwort funktioniert nicht: Lazy <T> verhindert nicht die mehrfache Ausführung der zwischengespeicherten Funktion. Siehe @nicker Antwort. [net core 2.0]
Fernando Silva
11

Überprüfen Sie diesen Link: http://msdn.microsoft.com/en-us/library/system.runtime.caching.memorycache(v=vs.110).aspx

Gehen Sie ganz nach unten auf der Seite (oder suchen Sie nach dem Text "Thread Safety").

Du wirst sehen:

^ Gewindesicherheit

Dieser Typ ist threadsicher.

EkoostikMartin
quelle
6
Ich habe vor einiger Zeit aufgehört, MSDNs Definition von "Thread Safe" allein aufgrund persönlicher Erfahrungen zu vertrauen. Hier ist eine gute Lektüre: Link
James Legan
3
Dieser Beitrag unterscheidet sich geringfügig von dem Link, den ich oben angegeben habe. Die Unterscheidung ist insofern sehr wichtig, als der von mir bereitgestellte Link keine Einschränkungen für die Erklärung der Gewindesicherheit enthielt. Ich habe auch persönliche Erfahrung mit MemoryCache.Defaultsehr hohem Volumen (Millionen von Cache-Treffern pro Minute) ohne Threading-Probleme.
EkoostikMartin
Ich denke, was sie bedeuten, ist, dass Lese- und Schreibvorgänge atomar ablaufen. Kurz gesagt, während Thread A versucht, den aktuellen Wert zu lesen, liest er immer abgeschlossene Schreibvorgänge, nicht während er Daten von einem Thread in den Speicher schreibt. Wenn Thread A es versucht Um in das Gedächtnis zu schreiben, konnte kein Thread eingreifen. Das ist mein Verständnis, aber es gibt viele Fragen / Artikel darüber, die nicht zu einer vollständigen Schlussfolgerung wie der folgenden führen. stackoverflow.com/questions/3137931/msdn-what-is-thread-safety
erhan355
2

Sie haben gerade eine Beispielbibliothek hochgeladen, um das Problem für .Net 2.0 zu beheben.

Schauen Sie sich dieses Repo an:

RedisLazyCache

Ich verwende den Redis-Cache, aber es ist auch ein Failover oder nur ein Memorycache, wenn der Verbindungsstring fehlt.

Es basiert auf der LazyCache-Bibliothek, die die einmalige Ausführung eines Rückrufs zum Schreiben im Falle eines Multithreading garantiert, bei dem versucht wird, Daten zu laden und zu speichern, insbesondere wenn die Ausführung des Rückrufs sehr teuer ist.

Francis Marasigan
quelle
Bitte nur Antwort teilen, andere Informationen können als Kommentar geteilt werden
ElasticCode
1
@WaelAbbas. Ich habe es versucht, aber es scheint, ich brauche zuerst 50 Ruf. : D. Obwohl es keine direkte Antwort auf die OP-Frage ist (kann mit Ja / Nein mit einer Erklärung beantwortet werden, warum), ist meine Antwort eine mögliche Lösung für die Nein-Antwort.
Francis Marasigan
1

Wie von @AmitE bei der Antwort von @pimbrouwers erwähnt, funktioniert sein Beispiel nicht wie hier gezeigt:

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

Ausgabe:

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

Es scheint, als würde GetOrCreateimmer der erstellte Eintrag zurückgegeben. Zum Glück ist das sehr einfach zu beheben:

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

Das funktioniert wie erwartet:

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
Kichern
quelle
Das funktioniert auch nicht. Versuchen Sie es einfach oft und Sie werden sehen, dass der Wert manchmal nicht immer 1 ist.
mlessard
1

Der Cache ist threadsicher, aber wie andere angegeben haben, ist es möglich, dass GetOrAdd die Funktion mehrere Typen aufruft, wenn mehrere Typen aufgerufen werden.

Hier ist meine minimale Lösung dafür

private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1);

und

await _cacheLock.WaitAsync();
var data = await _cache.GetOrCreateAsync(key, entry => ...);
_cacheLock.Release();
Anders
quelle
Ich denke, es ist eine nette Lösung, aber wenn ich mehrere Methoden habe, um verschiedene Caches zu ändern, werden sie ohne Notwendigkeit gesperrt, wenn ich die Sperre verwende! In diesem Fall sollten wir mehrere _cacheLocks haben, ich denke, es wäre besser, wenn der Cachelock auch einen Schlüssel haben könnte!
Daniel
Es gibt viele Möglichkeiten, es zu lösen. Eine davon ist generisch, dass das Semaphor pro Instanz von MyCache <T> eindeutig ist. Dann können Sie es registrieren AddSingleton (typeof (IMyCache <>), typeof (MyCache <>));
Anders
Ich würde wahrscheinlich nicht den gesamten Cache zu einem Singleton machen, der zu Problemen führen könnte, wenn Sie andere vorübergehende Typen aufrufen müssen. Vielleicht haben Sie einen Semaphor-Speicher ICacheLock <T>, der Singleton ist
Anders