it-swarm.com.de

MemoryCache-Thread-Sicherheit: Ist das Sperren erforderlich?

Für den Anfang lassen Sie mich einfach rausschmeißen, dass ich weiß, dass der folgende Code nicht fadensicher ist (Korrektur: möglicherweise). Was ich damit zu kämpfen habe, ist eine Implementierung zu finden, die ich im Test versagen kann. Ich überarbeite gerade ein großes WCF-Projekt, das einige (meistens) statische Daten benötigt, die zwischengespeichert und aus einer SQL-Datenbank ausgefüllt werden. Es muss mindestens einmal am Tag ablaufen und "aktualisiert" werden, weshalb ich MemoryCache verwende.

Ich weiß, dass der folgende Code nicht Thread-sicher sein sollte, aber ich kann nicht dazu führen, dass er unter hoher Last ausfällt und die Google-Suche kompliziert macht, und zeigt Implementierungen in beide Richtungen (mit und ohne Sperren, kombiniert mit Debatten, ob sie erforderlich sind oder nicht.

Könnte jemand mit MemoryCache-Kenntnissen in einer Multithread-Umgebung wissen, ob ich ggf. sperren muss, damit ein Aufruf zum Entfernen (der selten aufgerufen wird, aber seine Anforderung ist) beim Abrufen/Wiederauffü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;
    }
}
55
James Legan

Die von MS bereitgestellte Standardvariable MemoryCache ist absolut threadsicher. Jede benutzerdefinierte Implementierung, die von MemoryCache abgeleitet wird, ist möglicherweise nicht threadsicher. Wenn Sie plain MemoryCache verwenden, ist dies fadensicher. Durchsuchen Sie den Quellcode meiner verteilten Open Source-Caching-Lösung, um zu sehen, wie ich ihn verwende (MemCache.cs):

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

52
Haney

MemoryCache ist zwar zwar Thread-sicher, da andere Antworten angegeben haben, hat jedoch ein häufiges Multi-Threading-Problem. Wenn zwei Threads versuchen, Get aus dem Cache (oder markieren Sie Contains) gleichzeitig den Cache, werden beide den Cache verpassen und beide werden beendet Wenn Sie das Ergebnis generieren, fügen beide das Ergebnis zum Cache hinzu. 

Dies ist häufig unerwünscht - der zweite Thread sollte warten, bis der erste abgeschlossen ist und sein Ergebnis verwendet, anstatt zweimal Ergebnisse zu generieren. 

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

23
alastairtree

Wie andere bereits festgestellt haben, ist MemoryCache tatsächlich Thread-sicher. Die Gewindesicherheit der darin gespeicherten Daten liegt jedoch völlig in Ihrem Ermessen.

Reed Copsey aus seinem awesome post bezüglich Parallelität und dem ConcurrentDictionary<TKey, TValue> - Typ. Was hier natürlich gilt.

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

Sie können sich vorstellen, dass dies besonders schlecht wäre, wenn TValue in der Konstruktion teuer ist. 

Um dies zu umgehen, können Sie Lazy<T> sehr leicht einsetzen, was zufällig sehr kostengünstig 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 den gleichen singulären Lazy<T> an alle Threads zurück, die "zusätzlichen" Instanzen von Lazy<T> werden einfach weggeworfen.

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

Nun zu etwas Code! Im Folgenden finden Sie eine Erweiterungsmethode für IMemoryCache, die das Obige implementiert. Es ist willkürlich, SlidingExpiration basierend auf einer int seconds-Methode param zu setzen. 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 Stephen Toubs excellent AsyncLazy<T> -Implementierung in seinem Artikel auf MSDN. Welche den eingebauten Lazy-Initializer Lazy<T> mit dem Versprechen Task<T> kombiniert:

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())
    { }
}   

Nun 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());
11
pimbrouwers

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

Gehen Sie zum Ende der Seite (oder suchen Sie nach dem Text "Thread Safety").

Du wirst sehen:

^ Fadensicherheit

Dieser Typ ist threadsicher.

9
EkoostikMartin

Sie haben gerade eine Beispielbibliothek hochgeladen, um das Problem mit .Net 2.0 zu beheben. 

Schauen Sie sich dieses Repo an: 

RedisLazyCache

Ich verwende Redis-Cache, aber auch Failover oder nur Memorycache, wenn Connectionstring fehlt.

Es basiert auf der LazyCache-Bibliothek, die die einmalige Ausführung eines Callbacks zum Schreiben in einem Fall gewährleistet, in dem Multi-Threading versucht, Daten zu laden und zu speichern, insbesondere wenn der Callback sehr teuer ist. 

2

Der Cache ist threadsicher, aber wie andere angegeben haben, ist es möglich, dass GetOrAdd die Funktion für mehrere Typen aufruft, wenn der Aufruf von mehreren Typen erfolgt.

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();
0
Anders

Wie @AmitE in 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 GetOrCreate immer den erstellten Eintrag zurückgeben. 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
0
Snicker