Безпека потоків MemoryCache, чи потрібно блокування?


92

Для початку дозвольте мені просто викинути його туди, щоб я знав, що наведений нижче код не є безпечним для потоків (виправлення: можливо). Я борюся з тим, щоб знайти реалізацію, яка є такою, і яку я дійсно можу досягти, щоб не пройти тестування. Зараз я переробляю великий проект WCF, якому потрібні деякі (здебільшого) статичні дані, кешовані та заповнені з бази даних SQL. Він повинен закінчитися і "оновити" принаймні раз на день, саме тому я використовую MemoryCache.

Я знаю, що наведений нижче код не повинен бути безпечним для потоків, але я не можу змусити його вийти з ладу під великим навантаженням і ускладнити ситуацію, оскільки пошук у Google показує реалізації в обох напрямках (з блокуванням і без нього, а також суперечками про те, чи потрібні вони чи ні.

Чи міг хтось, хто знає MemoryCache у багатопотоковому середовищі, дати мені остаточно знати, чи потрібно мені зафіксувати, де це доречно, щоб виклик для видалення (який рідко називається, але це є вимогою) не перекидався під час пошуку / повторного заселення.

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 є потокобезпечним, я не думаю, що ваш клас може провалитися. msdn.microsoft.com/en-us/library/... Можливо, ви переходите до бази даних одночасно, але це використовуватиме лише більше процесора, ніж потрібно.
the_lotus

1
Хоча ObjectCache є потокобезпечним, його реалізації можуть бути не такими. Таким чином, питання MemoryCache.
Хейні

Відповіді:


79

За замовчуванням MS надається MemoryCacheповністю потокобезпечним. Будь-яка спеціальна реалізація, яка походить від, MemoryCacheможе бути не безпечною для потоків. Якщо ви використовуєте звичайну MemoryCacheкоробку, це безпечно для ниток. Перегляньте вихідний код мого рішення з розподіленим кешуванням з відкритим кодом, щоб побачити, як я його використовую (MemCache.cs):

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


2
Девід, Щоб підтвердити, у дуже простому прикладі класу, який я маю вище, виклик .Remove () насправді є безпечним для потоків, якщо інший потік знаходиться у процесі виклику Get ()? Думаю, мені слід просто використовувати рефлектор і копати глибше, але там багато суперечливої ​​інформації.
Джеймс Леган,

12
Це безпечно для потоків, але схильне до умов перегонів ... Якщо ваш Get відбувається до вашого Remove, дані будуть повернуті на Get. Якщо Видалення відбудеться першим, це не станеться. Це дуже схоже на брудне читання в базі даних.
Хейні

10
Варто згадати (як я зауважив в іншій відповіді нижче), реалізація ядра dotnet в даний час НЕ повністю безпечна для потоків. конкретно GetOrCreateметод. на github є проблема з цим
AmitE

Чи викидає кеш пам'яті виняток "З пам'яті" під час запуску потоків за допомогою консолі?
Faseeh Haris

50

Незважаючи на те, що MemoryCache дійсно є потокобезпечним, як вказали інші відповіді, він має загальну проблему багатопоточності - якщо 2 потоки намагаються одночасно Get(або перевірити Contains) кеш, тоді обидва будуть пропускати кеш, і обидва в кінцевому підсумку генерують результат і обидва потім додадуть результат до кешу.

Часто це небажано - другий потік повинен чекати завершення першого і використовувати його результат, а не генерувати результати двічі.

Це була одна з причин, чому я написав LazyCache - дружню обгортку на MemoryCache, яка вирішує подібні проблеми. Він також доступний на Nuget .


"у нього є загальна проблема багатопотоковості" Ось чому вам слід використовувати атомні методи типу AddOrGetExisting, замість того, щоб впроваджувати власну логіку навколо Contains. AddOrGetExistingМетод MemoryCache є атомним і THREADSAFE referencesource.microsoft.com/System.Runtime.Caching/R / ...
Алекс

1
Так AddOrGetExisting є потокобезпечним. Але передбачається, що у вас вже є посилання на об'єкт, який буде доданий до кешу. Зазвичай ви не хочете AddOrGetExisting, а "GetExistingOrGenerateThisAndCacheIt", що дає вам LazyCache.
alastairtree

так, домовилися щодо пункту "якщо у вас ще немає пункту об'єкту"
Олексій,

17

Як зазначали інші, MemoryCache справді є безпечним для потоків. Однак безпека потоків даних, що зберігаються в ньому, повністю залежить від вашого використання.

Цитую Ріда Копсі з його дивовижного допису щодо паралельності та ConcurrentDictionary<TKey, TValue>типу. Що, звичайно, застосовується тут.

Якщо два потоки викликають це [GetOrAdd] одночасно, можна легко створити два екземпляри TValue.

Ви можете собі уявити, що це було б особливо погано, якщо TValueбудівництво коштує дорого.

Щоб уникнути цього, ви можете скористатися вагомими ефектами Lazy<T> дуже легко , які, за збігом обставин, дуже дешево побудувати. Це робить гарантію, що якщо ми потрапляємо в багатопотокову ситуацію, ми будуємо лише кілька екземплярів Lazy<T>(що дешево).

GetOrAdd()( GetOrCreate()у випадку MemoryCache) поверне той самий, єдиний Lazy<T>для всіх потоків, "зайві" екземпляриLazy<T> просто викидаються.

Так як Lazy<T>не робить нічого до.Value буде викликано, коли-небудь будується лише один екземпляр об'єкта.

Тепер трохи коду! Нижче наведено метод розширення, для IMemoryCacheякого реалізовано вищезазначене. Він довільно встановлюється SlidingExpirationна основі int secondsпараметра методу. Але це цілком налаштовується залежно від ваших потреб.

Зверніть увагу, що це стосується додатків .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);
}

Дзвонити:

IMemoryCache cache;
var result = cache.GetOrAdd("someKey", 60, () => new object());

Щоб виконати це все асинхронно, я рекомендую використовувати чудову реалізацію Стівена Туба,AsyncLazy<T> знайдену в його статті про MSDN. Який поєднує вбудований ледачий ініціалізатор Lazy<T>із обіцянкою 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())
    { }
}   

Тепер асинхронна версія 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);
}

І нарешті, зателефонувати:

IMemoryCache cache;
var result = await cache.GetOrAddAsync("someKey", 60, async () => new object());

2
Я спробував, і, здається, це не працює (dot net core 2.0). кожен GetOrCreate створить новий екземпляр Lazy, а кеш оновиться новим Lazy, а отже, значення get обчислюється \ створюється кілька разів (у декількох потоках env).
AmitE

2
від поглядів його, .netcore 2.0 MemoryCache.GetOrCreateНЕ поточно таким же чином , ConcurrentDictionaryце
Amite

Можливо, дурне запитання, але чи впевнені ви, що це значення було створено кілька разів, а не Lazy? Якщо так, то як ви це підтвердили?
пім

3
Я використовував фабричну функцію, яка друкує на екрані, коли вона була використана + генерує випадкове число, і запускаю 10 потоків, всі намагаються GetOrCreateвикористовувати той самий ключ і той же завод. результат, фабрика була використана 10 разів при використанні з кешем пам’яті (побачив відбитки) + кожен раз, коли GetOrCreateповерталося інше значення! Я провів один і той же тест, використовуючи ConcurrentDicionaryі бачив, що фабрика використовується лише один раз, і завжди отримував одне і те ж значення. Я знайшов у ньому закрите видання в github, я просто написав там коментар, що його слід відкрити
AmitE

Попередження: ця відповідь не працює: Ледачий <T> не перешкоджає багаторазовому виконанню кешованої функції. Дивіться відповідь @snicker. [net core 2.0]
Фернандо Сільва,

11

Перевірте це посилання: http://msdn.microsoft.com/en-us/library/system.runtime.caching.memorycache(v=vs.110).aspx

Перейдіть до самого кінця сторінки (або знайдіть текст "Безпека ниток").

Ти побачиш:

^ Безпека ниток

Цей тип захищений від ниток.


7
Я перестав довіряти визначенню MSDN "Thread Safe" самостійно досить давно, базуючись на особистому досвіді. Ось гарне читання: посилання
Джеймс Леган,

3
Цей пост дещо відрізняється від посилання, яке я вказав вище. Різниця дуже важлива, оскільки посилання, яке я надав, не містить жодних застережень до декларації про безпеку ниток. У мене також є особистий досвід використання MemoryCache.Defaultу дуже великому обсязі (мільйони звернень кеш-пам’яті за хвилину) без проблем із потоками.
EkoostikMartin

Я думаю, що вони означають, що операції читання та запису відбуваються атомарно. Коротко кажучи, коли потік A намагається прочитати поточне значення, він завжди зчитує завершені операції запису, а не в середині запису даних в пам'ять будь-яким потоком. щоб записати в пам'ять, жодне втручання не може втручатися. Це я розумію, але є багато питань / статей щодо цього, які не призводять до повного висновку, як показано нижче. stackoverflow.com/questions/3137931/msdn-what-is-thread-safety
erhan355

3

Просто завантажив зразок бібліотеки для вирішення проблеми з .Net 2.0.

Погляньте на це репо:

RedisLazyCache

Я використовую кеш Redis, але він також переходить до відмов або просто Memorycache, якщо Connectionstring відсутній.

Він заснований на бібліотеці LazyCache, яка гарантує одноразове виконання зворотного виклику для запису у випадку багатопотокової спроби завантажити та зберегти дані, особливо якщо зворотний виклик дуже дорогий для виконання.


Будь ласка, повідомте лише відповідь, іншу інформацію можна надати як коментар
ElasticCode

1
@WaelAbbas. Я спробував, але, здається, мені спочатку потрібно 50 репутацій. : D. Хоча це не пряма відповідь на питання OP (на нього можна відповісти Так / Ні з деяким поясненням, чому), моя відповідь стосується можливого рішення відповіді Ні.
Френсіс Марасіган

1

Як згадував @AmitE у відповіді @pimbrouwers, його приклад не працює, як показано тут:

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

Вихід:

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

Здається, GetOrCreateповертає завжди створений запис. На щастя, це дуже легко виправити:

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

Це працює як слід:

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
Це теж не працює. Просто спробуйте багато разів, і ви побачите, що часом значення не завжди дорівнює 1.
mlessard

1

Кеш є безпечним для потоків, але, як зазначали інші, можливо, що GetOrAdd викликатиме функцію декількох типів, якщо буде здійснено дзвінок із декількох типів.

Ось мій мінімальний виправлення цього

private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1);

і

await _cacheLock.WaitAsync();
var data = await _cache.GetOrCreateAsync(key, entry => ...);
_cacheLock.Release();

Я вважаю це хорошим рішенням, але якщо у мене є кілька методів, які змінюють різні кеші, якщо я використовую блокування, вони будуть заблоковані без потреби! У цьому випадку у нас повинно бути кілька _cacheLocks, я думаю, було б краще, якби кешлок міг мати і Ключ!
Даніель

Існує безліч способів її вирішення, один із них - загальний, семафор буде унікальним для кожного екземпляра MyCache <T>. Тоді ви можете зареєструвати його AddSingleton (typeof (IMyCache <>), typeof (MyCache <>));
Андерс

Я, мабуть, не зробив би весь кеш єдиним символом, що може призвести до проблем, якщо вам потрібно буде викликати інші перехідні типи. Тож, можливо, у вас є магазин семафорів ICacheLock <T>, який є одиночним
Андерс,

Проблема цієї версії полягає в тому, що якщо у вас є дві різні речі для кешування одночасно, то вам доведеться почекати, поки перша закінчить генерацію, перш ніж ви зможете перевірити кеш для другої. Для них обох ефективніше мати можливість перевіряти кеш (і генерувати) одночасно, якщо ключі різні. LazyCache використовує комбінацію Lazy <T> та смугастого блокування, щоб забезпечити кешування ваших предметів якомога швидше та генерувати лише один раз на ключ. Дивіться github.com/alastairtree/lazycache
alastairtree
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.