Zakładam, że ten kod ma problemy ze współbieżnością:
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;
}
Przyczyną problemu ze współbieżnością jest to, że wiele wątków może uzyskać klucz o wartości null, a następnie próbować wstawić dane do pamięci podręcznej.
Jaki byłby najkrótszy i najczystszy sposób na udowodnienie współbieżności tego kodu? Lubię podążać za dobrym wzorcem w moim kodzie związanym z pamięcią podręczną. Bardzo pomocny byłby link do artykułu online.
AKTUALIZACJA:
Wymyśliłem ten kod na podstawie odpowiedzi @Scott Chamberlain. Czy ktoś może znaleźć w tym problem wydajności lub współbieżności? Jeśli to zadziała, zaoszczędziłoby to wiele linii kodu i błędów.
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
źródło
źródło
ReaderWriterLockSlim
?ReaderWriterLockSlim
również tej techniki, aby uniknąćtry-finally
stwierdzeń.Dictionary<string, object>
jeśli klucz jest tym samym kluczem, którego używasz w swoim,MemoryCache
a obiekt w słowniku to tylko podstawowy,Object
który blokujesz. Jednak biorąc to pod uwagę, polecam przeczytanie odpowiedzi Jona Hanny. Bez odpowiedniego profilowania możesz bardziej spowalniać program blokując niż pozwalając na dwie instancjeSomeHeavyAndExpensiveCalculation()
uruchomienia i mieć jeden wynik odrzucony.Odpowiedzi:
To moja druga iteracja kodu. Ponieważ
MemoryCache
jest bezpieczny wątkowo, nie musisz blokować początkowego odczytu, możesz po prostu przeczytać, a jeśli pamięć podręczna zwróci wartość null, wykonaj sprawdzenie blokady, aby zobaczyć, czy musisz utworzyć ciąg. To znacznie upraszcza kod.EDYCJA : Poniższy kod jest niepotrzebny, ale chciałem go zostawić, aby pokazać oryginalną metodę. Może to być przydatne dla przyszłych odwiedzających, którzy używają innej kolekcji, która ma odczyty bezpieczne dla wątków, ale zapisy bezpieczne bez wątków (prawie wszystkie klasy w
System.Collections
przestrzeni nazw są takie).Oto, jak bym to zrobił, używając
ReaderWriterLockSlim
do ochrony dostępu. Musisz zrobić coś w rodzaju „ podwójnie zaznaczonego blokowania ”, aby sprawdzić, czy ktoś inny utworzył buforowany element, podczas gdy my czekaliśmy na przejęcie blokady.źródło
MemoryCache
prawdopodobieństwo, że przynajmniej jedna z tych dwóch rzeczy była nie tak.Istnieje biblioteka open source [zastrzeżenie: które napisałem]: LazyCache, że IMO pokrywa twoje wymagania dwoma wierszami kodu:
Ma wbudowane blokowanie domyślnie, więc metoda buforowalna będzie wykonywana tylko raz na brak pamięci podręcznej i używa lambdy, więc możesz wykonać "pobierz lub dodaj" za jednym razem. Domyślnie upływa 20 minut.
Jest nawet pakiet NuGet ;)
źródło
Rozwiązałem ten problem, wykorzystując metodę AddOrGetExisting na MemoryCache i używając inicjalizacji Lazy .
Zasadniczo mój kod wygląda mniej więcej tak:
Najgorszy scenariusz jest taki, że ten sam
Lazy
obiekt tworzy się dwukrotnie. Ale to dość trywialne. UżycieAddOrGetExisting
gwarancji, że otrzymasz tylko jedno wystąpienieLazy
obiektu, więc masz również gwarancję, że tylko raz wywołasz kosztowną metodę inicjalizacji.źródło
SomeHeavyAndExpensiveCalculationThatResultsAString()
zgłosiłeś wyjątek, utknął w pamięci podręcznej. Nawet przejściowe wyjątki zostaną zapisane w pamięci podręcznejLazy<T>
: msdn.microsoft.com/en-us/library/vstudio/dd642331.aspxWłaściwie jest całkiem możliwe, ale z możliwą poprawą.
Ogólnie rzecz biorąc, wzorzec, w którym mamy wiele wątków ustawiających wspólną wartość przy pierwszym użyciu, aby nie blokować uzyskiwanej i ustawianej wartości, może wyglądać następująco:
Jednak biorąc pod uwagę, że
MemoryCache
może to spowodować eksmisję wpisów, to:MemoryCache
jest jest to niewłaściwe podejście.MemoryCache
jest bezpieczny dla wątków pod względem dostępu do tego obiektu, więc nie stanowi to problemu.Oczywiście należy wziąć pod uwagę obie te możliwości, chociaż jedyny problem z dwoma instancjami tego samego ciągu może stanowić problem, jeśli wykonujesz bardzo szczegółowe optymalizacje, które nie mają tutaj zastosowania *.
Tak więc pozostały nam możliwości:
SomeHeavyAndExpensiveCalculation()
.SomeHeavyAndExpensiveCalculation()
.Wypracowanie tego może być trudne (w istocie jest to rzecz, w której warto profilować, zamiast zakładać, że możesz to rozwiązać). Warto tutaj jednak wziąć pod uwagę, że najbardziej oczywiste sposoby blokowania wkładki zapobiegną wszystkim dodatkom do pamięci podręcznej, w tym niepowiązanym.
Oznacza to, że gdybyśmy mieli 50 wątków próbujących ustawić 50 różnych wartości, musielibyśmy sprawić, że wszystkie 50 wątków zaczeka na siebie, nawet jeśli nie zamierzały nawet wykonywać tych samych obliczeń.
W związku z tym prawdopodobnie lepiej będzie, jeśli masz kod, niż kod, który unika warunków wyścigu, a jeśli stan wyścigu jest problemem, prawdopodobnie musisz sobie z tym poradzić gdzie indziej lub potrzebujesz innego strategia buforowania niż taka, która usuwa stare wpisy †.
Jedyną rzeczą, którą bym zmienił, jest zamiana połączenia na
Set()
jeden doAddOrGetExisting()
. Z powyższego powinno być jasne, że prawdopodobnie nie jest to konieczne, ale pozwoliłoby na zebranie nowo uzyskanego elementu, zmniejszając ogólne użycie pamięci i pozwalając na wyższy stosunek kolekcji niskiej generacji do kolekcji wysokiej generacji.Więc tak, możesz użyć podwójnego blokowania, aby zapobiec współbieżności, ale albo współbieżność w rzeczywistości nie jest problemem, albo twoje przechowywanie wartości w niewłaściwy sposób, albo podwójne blokowanie w sklepie nie byłoby najlepszym sposobem rozwiązania tego problemu .
* Jeśli wiesz, że istnieje tylko jeden z każdego zestawu ciągów, możesz zoptymalizować porównania równości, co jest mniej więcej jedynym przypadkiem, gdy posiadanie dwóch kopii łańcucha może być niepoprawne, a nie tylko nieoptymalne, ale chciałbyś to zrobić bardzo różne typy buforowania, aby miało to sens. Np. Rodzaj
XmlReader
działa wewnętrznie.† Całkiem prawdopodobne, że albo taki, który przechowuje na czas nieokreślony, albo taki, który korzysta ze słabych odniesień, więc usunie wpisy tylko wtedy, gdy nie ma istniejących zastosowań.
źródło
Aby uniknąć globalnej blokady, możesz użyć SingletonCache do zaimplementowania jednej blokady na klucz, bez eksplodującego użycia pamięci (obiekty blokady są usuwane, gdy nie są już przywoływane, a pobieranie / zwalnianie jest bezpieczne wątkowo, co gwarantuje, że tylko 1 wystąpienie jest kiedykolwiek używane poprzez porównanie i zamień).
Używanie go wygląda następująco:
Kod jest tutaj na GitHub: https://github.com/bitfaster/BitFaster.Caching
Istnieje również implementacja LRU, która jest lżejsza niż MemoryCache i ma kilka zalet - szybsze równoczesne odczyty i zapisy, ograniczony rozmiar, brak wątku w tle, wewnętrzne liczniki wydajności itp. (Zastrzeżenie, napisałem to).
źródło
Przykładem konsola z MemoryCache „Jak zapisać / dostać prostych obiektów klasy”
Wyjście po uruchomieniu i naciśnięciu Any keyz wyjątkiem Esc:
Zapisuję do pamięci podręcznej!
Pobieranie z pamięci podręcznej!
some1
Some2
źródło
źródło
Jednak trochę za późno ... Pełna realizacja:
Oto
getPageContent
podpis:A oto
MemoryCacheWithPolicy
realizacja:nlogger
jest po prostunLog
obiektem do śledzeniaMemoryCacheWithPolicy
zachowania. Ponownie tworzę pamięć podręczną, jeśli obiekt żądania (RequestQuery requestQuery
) zostanie zmieniony przez delegate (Func<TParameter, TResult> createCacheData
) lub ponownie utworzę, gdy przesunięcie lub bezwzględny czas osiągnął swój limit. Zauważ, że wszystko też jest asynchroniczne;)źródło
Trudno jest wybrać, który jest lepszy; lock lub ReaderWriterLockSlim. Potrzebujesz rzeczywistych statystyk dotyczących odczytu i zapisu liczb i współczynników itp.
Ale jeśli uważasz, że użycie „zamka” jest właściwym sposobem. Tutaj jest inne rozwiązanie dla różnych potrzeb. Do kodu dołączam również rozwiązanie Allana Xu. Ponieważ oba mogą być potrzebne do różnych potrzeb.
Oto wymagania, które prowadzą mnie do tego rozwiązania:
Kod:
źródło