Подтвердить что ты не робот

Безопасность памяти MemoryCache, требуется ли блокировка?

Для начала позвольте мне просто выбросить туда, что я знаю, что код ниже не является потокобезопасным (исправление: может быть). То, с чем я борюсь, - это найти реализацию, которая есть, и то, что я действительно могу пройти с ошибкой. Я сейчас реорганизую большой проект 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;
    }
}
4b9b3361

Ответ 1

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

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

Ответ 2

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

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

Это была одна из причин, по которой я писал LazyCache - дружественную оболочку MemoryCache, которая решает эти проблемы. Он также доступен на Nuget.

Ответ 3

Как уже говорили другие, MemoryCache действительно поточно-ориентированный. Тем не менее, безопасность потоков данных, хранящихся в нем, полностью зависит от того, как вы их используете.

Цитирую Рида Копси из его замечательного поста post о параллелизме и типе 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());

Ответ 5

Только что загрузил пример библиотеки для решения проблемы .Net 2.0.

Посмотрите на этот репо:

RedisLazyCache

Я использую кэш Redis, но он также выполняет отработку отказа или просто кэш памяти, если отсутствует строка подключения.

Он основан на библиотеке LazyCache, которая гарантирует однократное выполнение обратного вызова для записи в случае многопоточности, пытающейся загрузить и сохранить данные, особенно если обратный вызов очень дорогой для выполнения.

Ответ 6

Как отметил @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

Ответ 7

Кэш является поточно-ориентированным, но, как утверждали другие, возможно, что GetOrAdd будет вызывать функцию нескольких типов, если она вызывается из нескольких типов.

Вот мое минимальное исправление на этом

private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1);

и

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