Ich gehe davon aus, dass dieser Code Probleme mit der Parallelität hat:
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;
}
Der Grund für das Problem der Parallelität besteht darin, dass mehrere Threads einen Nullschlüssel erhalten und dann versuchen können, Daten in den Cache einzufügen.
Was wäre der kürzeste und sauberste Weg, um diesen Code gleichzeitig zu prüfen? Ich folge gerne einem guten Muster in meinem Cache-Code. Ein Link zu einem Online-Artikel wäre eine große Hilfe.
AKTUALISIEREN:
Ich habe diesen Code basierend auf der Antwort von @Scott Chamberlain entwickelt. Kann jemand ein Leistungs- oder Parallelitätsproblem damit finden? Wenn dies funktioniert, würden viele Codezeilen und Fehler eingespart.
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
quelle
quelle
ReaderWriterLockSlim
?ReaderWriterLockSlim
... Aber ich würde diese Technik auch verwenden , umtry-finally
Aussagen zu vermeiden .Dictionary<string, object>
Schlüssel durchgeführt werden, bei dem der Schlüssel derselbe ist, den Sie in Ihrem verwenden,MemoryCache
und das Objekt im Wörterbuch nur eine Grundvoraussetzung ist, dieObject
Sie festlegen. Trotzdem würde ich Ihnen empfehlen, Jon Hannas Antwort durchzulesen. Ohne eine ordnungsgemäße Profilierung verlangsamen Sie Ihr Programm möglicherweise mehr durch Sperren als durch Anlassen von zwei LaufinstanzenSomeHeavyAndExpensiveCalculation()
und lassen ein Ergebnis wegwerfen.Antworten:
Dies ist meine 2. Iteration des Codes. Da
MemoryCache
Thread-sicher ist, müssen Sie beim ersten Lesen nicht sperren. Sie können nur lesen. Wenn der Cache null zurückgibt, führen Sie die Sperrprüfung durch, um festzustellen, ob Sie die Zeichenfolge erstellen müssen. Dies vereinfacht den Code erheblich.BEARBEITEN : Der folgende Code ist nicht erforderlich, aber ich wollte ihn belassen, um die ursprüngliche Methode zu zeigen. Dies kann für zukünftige Besucher nützlich sein, die eine andere Sammlung verwenden, die threadsichere Lesevorgänge, aber nicht threadsichere Schreibvorgänge enthält (fast alle Klassen unter dem
System.Collections
Namespace sind so).Hier ist, wie ich es tun würde
ReaderWriterLockSlim
, um den Zugriff zu schützen. Sie müssen eine Art " Double Checked Locking " durchführen, um zu sehen, ob jemand anderes das zwischengespeicherte Element erstellt hat, während wir darauf warten, das Schloss zu öffnen.quelle
MemoryCache
Chance geraten, ist mindestens eines dieser beiden Dinge falsch.Es gibt eine Open-Source-Bibliothek [Haftungsausschluss: den ich geschrieben habe]: LazyCache, dass IMO Ihre Anforderungen mit zwei Codezeilen abdeckt:
Standardmäßig ist eine Sperre integriert, sodass die zwischenspeicherbare Methode nur einmal pro Cache-Fehler ausgeführt wird. Außerdem wird ein Lambda verwendet, sodass Sie "get or add" auf einmal ausführen können. Der Standardwert beträgt 20 Minuten.
Es gibt sogar ein NuGet-Paket ;)
quelle
Ich habe dieses Problem gelöst, indem ich die AddOrGetExisting- Methode im MemoryCache und die Lazy-Initialisierung verwendet habe .
Im Wesentlichen sieht mein Code ungefähr so aus:
Im schlimmsten Fall erstellen Sie dasselbe
Lazy
Objekt zweimal. Das ist aber ziemlich trivial. Die Verwendung vonAddOrGetExisting
garantiert, dass Sie immer nur eine Instanz desLazy
Objekts erhalten, sodass Sie die teure Initialisierungsmethode garantiert nur einmal aufrufen.quelle
SomeHeavyAndExpensiveCalculationThatResultsAString()
eine Ausnahme ausgelöst wird, bleibt sie im Cache stecken. Selbst vorübergehende Ausnahmen werden zwischengespeichert mitLazy<T>
: msdn.microsoft.com/en-us/library/vstudio/dd642331.aspxEigentlich ist es durchaus in Ordnung, wenn auch mit einer möglichen Verbesserung.
Im Allgemeinen kann das Muster, bei dem mehrere Threads bei der ersten Verwendung einen gemeinsamen Wert festlegen, um den erhaltenen und festgelegten Wert nicht zu sperren, Folgendes sein:
In
MemoryCache
Anbetracht dessen, dass Einträge dann möglicherweise entfernt werden:MemoryCache
ist dies der falsche Ansatz.MemoryCache
ist in Bezug auf den Zugriff auf dieses Objekt threadsicher, sodass dies hier kein Problem darstellt.Beide Möglichkeiten müssen natürlich in Betracht gezogen werden, obwohl das einzige Mal, wenn zwei Instanzen derselben Zeichenfolge vorhanden sind, ein Problem sein kann, wenn Sie ganz bestimmte Optimierungen vornehmen, die hier nicht zutreffen *.
Wir haben also die Möglichkeit:
SomeHeavyAndExpensiveCalculation()
.SomeHeavyAndExpensiveCalculation()
.Und das herauszufinden kann schwierig sein (in der Tat die Art von Dingen, bei denen es sich lohnt, ein Profil zu erstellen, anstatt davon auszugehen, dass Sie es herausfinden können). Es ist jedoch erwägenswert, dass die offensichtlichsten Methoden zum Sperren beim Einfügen alle Ergänzungen zum Cache verhindern, einschließlich solcher, die nichts miteinander zu tun haben.
Das heißt, wenn wir 50 Threads hatten, die versuchten, 50 verschiedene Werte festzulegen, müssen wir alle 50 Threads aufeinander warten lassen, obwohl sie nicht einmal dieselbe Berechnung durchführen würden.
Als solches sind Sie mit dem Code, den Sie haben, wahrscheinlich besser dran als mit Code, der die Rennbedingung vermeidet, und wenn die Rennbedingung ein Problem darstellt, müssen Sie dies wahrscheinlich entweder woanders behandeln oder benötigen eine andere Caching-Strategie als eine, die alte Einträge ausschließt †.
Das einzige, was ich ändern würde, wäre, den Anruf
Set()
durch einen zu ersetzenAddOrGetExisting()
. Aus dem oben Gesagten sollte klar sein, dass dies wahrscheinlich nicht erforderlich ist, aber es würde ermöglichen, das neu erhaltene Objekt zu sammeln, was die Gesamtspeicherauslastung verringert und ein höheres Verhältnis von Sammlungen mit niedriger zu hoher Generation ermöglicht.Ja, Sie könnten die doppelte Sperre verwenden, um Parallelität zu verhindern, aber entweder ist die Parallelität eigentlich kein Problem, oder Sie speichern die Werte falsch, oder die doppelte Sperre im Geschäft wäre nicht der beste Weg, dies zu lösen .
* Wenn Sie wissen, dass jeweils nur eine Zeichenfolge vorhanden ist, können Sie Gleichheitsvergleiche optimieren. Dies ist ungefähr das einzige Mal, dass zwei Kopien einer Zeichenfolge falsch und nicht nur suboptimal sein können, aber Sie möchten dies tun sehr unterschiedliche Arten des Cachings, damit dies Sinn macht. ZB die Sortierung
XmlReader
intern.† Sehr wahrscheinlich entweder eine, die auf unbestimmte Zeit gespeichert wird, oder eine, die schwache Referenzen verwendet, sodass Einträge nur dann ausgeschlossen werden, wenn keine Verwendungen vorhanden sind.
quelle
Um die globale Sperre zu vermeiden, können Sie SingletonCache verwenden, um eine Sperre pro Schlüssel zu implementieren, ohne die Speichernutzung zu explodieren (die Sperrobjekte werden entfernt, wenn nicht mehr darauf verwiesen wird, und das Erfassen / Freigeben ist threadsicher und garantiert, dass nur 1 Instanz jemals über Vergleich verwendet wird und tauschen).
Die Verwendung sieht folgendermaßen aus:
Code ist hier auf GitHub: https://github.com/bitfaster/BitFaster.Caching
Es gibt auch eine LRU-Implementierung, die leichter als MemoryCache ist und mehrere Vorteile bietet - schnellere gleichzeitige Lese- und Schreibvorgänge, begrenzte Größe, kein Hintergrund-Thread, interne Leistungsindikatoren usw. (Haftungsausschluss, ich habe ihn geschrieben).
quelle
Konsolenbeispiel für MemoryCache , "Speichern / Abrufen einfacher Klassenobjekte"
Ausgabe nach dem Starten und Drücken Any keyaußer Esc:
Speichern im Cache!
Aus dem Cache!
Some1
Some2
quelle
quelle
Es ist jedoch etwas spät ... Vollständige Implementierung:
Hier ist die
getPageContent
Unterschrift:Und hier ist die
MemoryCacheWithPolicy
Implementierung:nlogger
ist nur einnLog
Objekt, um dasMemoryCacheWithPolicy
Verhalten zu verfolgen . Ich erstelle den Speichercache neu, wenn das Anforderungsobjekt (RequestQuery requestQuery
) durch den Delegaten (Func<TParameter, TResult> createCacheData
) geändert wird, oder erstelle es neu, wenn die gleitende oder absolute Zeit ihr Limit erreicht hat. Beachten Sie, dass auch alles asynchron ist;)quelle
Es ist schwierig zu entscheiden, welches besser ist; lock oder ReaderWriterLockSlim. Sie benötigen reale Statistiken über Lese- und Schreibzahlen und -verhältnisse usw.
Aber wenn Sie glauben, dass die Verwendung von "Schloss" der richtige Weg ist. Dann ist hier eine andere Lösung für unterschiedliche Bedürfnisse. Ich füge auch die Allan Xu-Lösung in den Code ein. Weil beide für unterschiedliche Bedürfnisse benötigt werden können.
Hier sind die Anforderungen, die mich zu dieser Lösung führen:
Code:
quelle