Bezpieczeństwo wątków pamięci podręcznej - czy konieczne jest blokowanie?


91

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

ObjectCache jest bezpieczny dla wątków, nie sądzę, że twoja klasa może zawieść. msdn.microsoft.com/en-us/library/… Być może będziesz przechodził do bazy danych w tym samym czasie, ale zużyje to tylko więcej procesora niż potrzeba.
the_lotus

1
Chociaż ObjectCache jest bezpieczny wątkowo, jego implementacje mogą nie być. Stąd pytanie MemoryCache.
Haney

Odpowiedzi:


78

Domyślnie dostarczony przez MS MemoryCachejest całkowicie bezpieczny dla wątków. Każda implementacja niestandardowa pochodząca z MemoryCachemoże nie być bezpieczna wątkowo. Jeśli używasz zwykłego po MemoryCachewyję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


2
David, Aby potwierdzić, w bardzo prostej klasie przykładowej, którą mam powyżej, wywołanie .Remove () jest w rzeczywistości bezpieczne dla wątków, jeśli inny wątek jest w trakcie wywoływania Get ()? Przypuszczam, że powinienem po prostu użyć reflektora i kopać głębiej, ale jest tam wiele sprzecznych informacji.
James Legan

12
Jest bezpieczny dla wątków, ale podatny na warunki wyścigu ... Jeśli Get nastąpi przed usunięciem, dane zostaną zwrócone w Get. Jeśli usunięcie nastąpi jako pierwsze, nie nastąpi. To bardzo przypomina brudne odczyty w bazie danych.
Haney

10
Warto wspomnieć (jak skomentowałem w innej odpowiedzi poniżej), implementacja rdzenia dotnet NIE jest obecnie całkowicie bezpieczna dla wątków. konkretnie GetOrCreatemetoda. na
githubie

Czy pamięć podręczna zgłasza wyjątek „Brak pamięci” podczas uruchamiania wątków przy użyciu konsoli?
Faseeh Haris

49

Chociaż 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ą Getz (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 .


„ma wspólny problem z wielowątkowością”. Dlatego powinieneś używać metod atomowych, takich jak AddOrGetExisting, zamiast implementować własną logikę Contains. AddOrGetExistingMetoda MemoryCache jest atomowa i THREADSAFE referencesource.microsoft.com/System.Runtime.Caching/R/...
Alex

1
Tak AddOrGetExisting jest bezpieczny dla wątków. Ale zakłada, że ​​masz już odniesienie do obiektu, który zostałby dodany do pamięci podręcznej. Zwykle nie chcesz AddOrGetExisting, ale chcesz „GetExistingOrGenerateThisAndCacheIt”, co daje ci LazyCache.
alastairtree

tak, zgodził się na punkt "jeśli jeszcze nie masz obiektu"
Alex

17

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.

Jeśli dwa wątki wywołują to [GetOrAdd] jednocześnie, można łatwo skonstruować dwie instancje TValue.

Możesz sobie wyobrazić, że byłoby to szczególnie złe, gdyby TValuebył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 instancji Lazy<T>(co jest tanie).

GetOrAdd()( GetOrCreate()w przypadku MemoryCache) zwróci to samo, pojedyncze Lazy<T>dla wszystkich wątków, „dodatkowe” wystąpienia Lazy<T>są po prostu odrzucane.

Ponieważ obiekt Lazy<T>nie robi nic, dopóki nie .Valuezostanie wywołany, tylko jedna instancja obiektu jest tworzona.

Teraz trochę kodu! Poniżej znajduje się metoda rozszerzenia, w IMemoryCachektórej zaimplementowano powyższe. Jest to ustawienie arbitralne w SlidingExpirationoparciu o int secondsparametr metody. Ale jest to całkowicie konfigurowalne w zależności od twoich potrzeb.

Zauważ, że jest to specyficzne dla aplikacji .netcore2.0

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żnaAsyncLazy<T> znaleźć w jego artykule na MSDN. Który łączy wbudowany leniwy inicjator Lazy<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());

2
Wypróbowałem to i wygląda na to, że nie działa (dot net core 2.0). każde GetOrCreate utworzy nową instancję Lazy, a pamięć podręczna zostanie zaktualizowana o nową Lazy, a więc wartość get została oszacowana \ utworzona wiele razy (w wielowątkowej env).
AmitE

2
z niecierpliwością na to, .netcore 2.0 MemoryCache.GetOrCreatenie jest bezpieczeństwo wątków w taki sam sposób ConcurrentDictionaryjest
Amite

Prawdopodobnie głupie pytanie, ale czy jesteś pewien, że to Wartość, która została utworzona wiele razy, a nie Lazy? Jeśli tak, w jaki sposób to potwierdziłeś?
pim

3
Użyłem funkcji fabrycznej, która drukuje na ekranie, gdy jest używana, + generuje liczbę losową i uruchamia 10 wątków, wszystkie próbując GetOrCreateuż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 razem GetOrCreatezwracano inną wartość! Przeprowadziłem ten sam test, używając ConcurrentDicionaryi 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 otwarty
AmitE

Ostrzeżenie: ta odpowiedź nie działa: Lazy <T> nie zapobiega wielokrotnemu wykonywaniu funkcji buforowanej. Zobacz odpowiedź @snicker. [net core 2.0]
Fernando Silva,

11

Sprawdź 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:

^ Bezpieczeństwo wątku

Ten typ jest bezpieczny dla wątków.


7
Już jakiś czas temu przestałem ufać samej definicji MSDN „Thread Safe” w oparciu o osobiste doświadczenia. Oto dobra lektura: link
James Legan

3
Ten post jest nieco inny niż link, który podałem powyżej. Rozróżnienie jest bardzo ważne, ponieważ podany przeze mnie link nie zawierał żadnych zastrzeżeń co do deklaracji bezpieczeństwa nici. Mam również osobiste doświadczenie w korzystaniu MemoryCache.Defaultz bardzo dużej ilości (miliony trafień w pamięci podręcznej na minutę) bez żadnych problemów z wątkami.
EkoostikMartin

Myślę, że mają na myśli to, że operacje odczytu i zapisu odbywają się niepodzielnie. Krótko mówiąc, podczas gdy wątek A próbuje odczytać bieżącą wartość, zawsze czyta zakończone operacje zapisu, a nie w trakcie zapisywania danych do pamięci przez dowolny wątek. aby pisać do pamięci, żadna interwencja nie mogła być dokonana przez żaden wątek.To rozumiem, ale jest wiele pytań / artykułów na ten temat, które nie prowadzą do pełnego wniosku, takiego jak śledzenie. stackoverflow.com/questions/3137931/msdn-what-is-thread-safety
erhan355

3

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.


Proszę podać tylko odpowiedź, inne informacje można udostępnić jako komentarz
ElasticCode

1
@WaelAbbas. Próbowałem, ale wygląda na to, że najpierw potrzebuję 50 reputacji. :RE. Chociaż nie jest to bezpośrednia odpowiedź na pytanie PO (można na nie odpowiedzieć tak / nie z pewnym wyjaśnieniem dlaczego), moja odpowiedź jest na możliwe rozwiązanie odpowiedzi na odpowiedź „Nie”.
Francis Marasigan

1

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 GetOrCreatezawsze 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

2
To też nie działa. Po prostu spróbuj wiele razy, a zobaczysz, że czasami wartość nie zawsze wynosi 1.
mlessard

1

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

Myślę, że to fajne rozwiązanie, ale jeśli mam wiele metod zmiany różnych pamięci podręcznych, jeśli użyję blokady, zostaną zablokowane bez potrzeby! W tym przypadku powinniśmy mieć wiele _cacheLocks, myślę, że byłoby lepiej, gdyby cachelock mógł mieć również klucz!
Daniel

Istnieje wiele sposobów rozwiązania tego problemu, jeden jest generyczny, semafor będzie unikalny dla instancji MyCache <T>. Następnie możesz go zarejestrować AddSingleton (typeof (IMyCache <>), typeof (MyCache <>));
Anders

Prawdopodobnie nie uczyniłbym całej pamięci podręcznej singletonem, który mógłby spowodować problemy, gdybyś musiał wywołać inne typy przejściowe. Więc może mieć magazyn semaforów ICacheLock <T>, który jest singletonem
Anders

Problem z tą wersją polega na tym, że jeśli masz 2 różne rzeczy do buforowania w tym samym czasie, musisz poczekać, aż pierwsza zakończy generowanie, zanim będziesz mógł sprawdzić pamięć podręczną dla drugiej. Bardziej wydajne jest dla nich obu, aby mogli jednocześnie sprawdzić pamięć podręczną (i wygenerować), jeśli klucze są różne. LazyCache używa kombinacji Lazy <T> i blokowania rozłożonego, aby zapewnić jak najszybsze buforowanie elementów i generowanie tylko raz dla każdego klucza. Zobacz github.com/alastairtree/lazycache
alastairtree
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.