Na początek pozwolę sobie wyrzucić, że wiem, że poniższy kod nie jest bezpieczny dla wątków (poprawka: może być). To, z czym się zmagam, to znalezienie implementacji, która jest taka, której faktycznie mogę się nie udać podczas testów. W tej chwili refaktoryzuję duży projekt WCF, który potrzebuje (głównie) danych statycznych w pamięci podręcznej i jest wypełniany z bazy danych SQL. Musi wygasać i "odświeżyć" przynajmniej raz dziennie, dlatego używam MemoryCache.
Wiem, że poniższy kod nie powinien być bezpieczny dla wątków, ale nie mogę sprawić, że zawiedzie przy dużym obciążeniu i komplikuje sprawy, wyszukiwarka Google pokazuje implementacje w obie strony (z blokadami i bez nich połączone z debatami, czy są one konieczne, czy nie).
Czy ktoś ze znajomością MemoryCache w środowisku wielowątkowym mógłby dać mi definitywnie znać, czy muszę blokować tam, gdzie to konieczne, aby wywołanie usunięcia (które rzadko będzie wywoływane, ale jest to wymagane) nie zostanie wyrzucone podczas pobierania / repopulacji.
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;
}
}
źródło
Odpowiedzi:
Domyślnie dostarczony przez MS
MemoryCache
jest całkowicie bezpieczny dla wątków. Każda implementacja niestandardowa pochodząca zMemoryCache
może nie być bezpieczna wątkowo. Jeśli używasz zwykłego poMemoryCache
wyjęciu z pudełka, jest bezpieczny. Przejrzyj kod źródłowy mojego rozwiązania do rozproszonej pamięci podręcznej typu open source, aby zobaczyć, jak go używam (MemCache.cs):https://github.com/haneytron/dache/blob/master/Dache.CacheHost/Storage/MemCache.cs
źródło
GetOrCreate
metoda. naChociaż MemoryCache jest rzeczywiście bezpieczny wątkowo, jak wskazały inne odpowiedzi, ma wspólny problem z wielowątkowością - jeśli 2 wątki próbują
Get
z (lub sprawdźContains
) pamięć podręczną w tym samym czasie, oba przegapią pamięć podręczną i oba zakończą się generowaniem wynik, a następnie dodają wynik do pamięci podręcznej.Często jest to niepożądane - drugi wątek powinien poczekać na zakończenie pierwszego i wykorzystać jego wynik zamiast dwukrotnie generować wyniki.
To był jeden z powodów, dla których napisałem LazyCache - przyjazne opakowanie na MemoryCache, które rozwiązuje tego rodzaju problemy. Jest również dostępny na Nuget .
źródło
AddOrGetExisting
, zamiast implementować własną logikęContains
.AddOrGetExisting
Metoda MemoryCache jest atomowa i THREADSAFE referencesource.microsoft.com/System.Runtime.Caching/R/...Jak powiedzieli inni, MemoryCache jest rzeczywiście bezpieczna wątkowo. Bezpieczeństwo wątków przechowywanych w nim danych zależy jednak wyłącznie od tego, jak je wykorzystasz.
Cytując Reeda Copseya z jego niesamowitego postu dotyczącego współbieżności i
ConcurrentDictionary<TKey, TValue>
typu. Co oczywiście ma tutaj zastosowanie.Możesz sobie wyobrazić, że byłoby to szczególnie złe, gdyby
TValue
było drogie w budowie.Aby obejść ten problem, możesz
Lazy<T>
bardzo łatwo wykorzystać dźwignię , co przypadkowo jest bardzo tanie w budowie. Dzięki temu, jeśli znajdziemy się w sytuacji wielowątkowej, budujemy tylko wiele instancjiLazy<T>
(co jest tanie).GetOrAdd()
(GetOrCreate()
w przypadkuMemoryCache
) zwróci to samo, pojedynczeLazy<T>
dla wszystkich wątków, „dodatkowe” wystąpieniaLazy<T>
są po prostu odrzucane.Ponieważ obiekt
Lazy<T>
nie robi nic, dopóki nie.Value
zostanie wywołany, tylko jedna instancja obiektu jest tworzona.Teraz trochę kodu! Poniżej znajduje się metoda rozszerzenia, w
IMemoryCache
której zaimplementowano powyższe. Jest to ustawienie arbitralne wSlidingExpiration
oparciu oint seconds
parametr metody. Ale jest to całkowicie konfigurowalne w zależności od twoich potrzeb.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); }
Zadzwonić:
IMemoryCache cache; var result = cache.GetOrAdd("someKey", 60, () => new object());
Aby wykonać to wszystko asynchronicznie, polecam użycie doskonałej implementacji Stephena Touba, którą można
AsyncLazy<T>
znaleźć w jego artykule na MSDN. Który łączy wbudowany leniwy inicjatorLazy<T>
z obietnicą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()) { } }
Teraz wersja asynchroniczna
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); }
I na koniec zadzwonić:
IMemoryCache cache; var result = await cache.GetOrAddAsync("someKey", 60, async () => new object());
źródło
MemoryCache.GetOrCreate
nie jest bezpieczeństwo wątków w taki sam sposóbConcurrentDictionary
jestLazy
? Jeśli tak, w jaki sposób to potwierdziłeś?GetOrCreate
użyć tego samego klucza i tej fabryki. w rezultacie fabryka była używana 10 razy przy użyciu z pamięcią podręczną (widziałem wydruki) + za każdym razemGetOrCreate
zwracano inną wartość! Przeprowadziłem ten sam test, używającConcurrentDicionary
i zobaczyłem, że fabryka była używana tylko raz i zawsze otrzymywałem tę samą wartość. Znalazłem zamknięty numer na githubie, właśnie napisałem tam komentarz, że powinien zostać ponownie otwartySprawdź ten link: http://msdn.microsoft.com/en-us/library/system.runtime.caching.memorycache(v=vs.110).aspx
Przejdź na sam dół strony (lub wyszukaj tekst „Bezpieczeństwo wątków”).
Zobaczysz:
źródło
MemoryCache.Default
z bardzo dużej ilości (miliony trafień w pamięci podręcznej na minutę) bez żadnych problemów z wątkami.Właśnie przesłałem bibliotekę próbek, aby rozwiązać problem z .Net 2.0.
Spójrz na to repozytorium:
RedisLazyCache
Używam pamięci podręcznej Redis, ale jest to również przełączanie awaryjne lub po prostu pamięć podręczna, jeśli brakuje ciągu połączenia.
Opiera się na bibliotece LazyCache, która gwarantuje pojedyncze wykonanie wywołania zwrotnego do zapisu w przypadku wielowątkowej próby załadowania i zapisania danych, szczególnie jeśli wywołanie zwrotne jest bardzo kosztowne w wykonaniu.
źródło
Jak wspomniał @AmitE w odpowiedzi @pimbrouwers, jego przykład nie działa, jak pokazano tutaj:
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; } }
Wynik:
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
Wygląda na to, że
GetOrCreate
zawsze zwraca utworzony wpis. Na szczęście bardzo łatwo to naprawić: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; }
Działa zgodnie z oczekiwaniami:
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
źródło
Pamięć podręczna jest wątkowo bezpieczna, ale jak stwierdzili inni, możliwe jest, że GetOrAdd wywoła funkcję wielu typów w przypadku wywołania z wielu typów.
Oto moja minimalna poprawka
private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1);
i
await _cacheLock.WaitAsync(); var data = await _cache.GetOrCreateAsync(key, entry => ...); _cacheLock.Release();
źródło