2013-11-22 3 views
40

Для начала позвольте мне просто выбросить туда, что я знаю, что код ниже не является потокобезопасным (исправление: может быть). То, с чем я борюсь, - это найти реализацию, которая есть, и то, что я действительно могу пройти с ошибкой. Я сейчас реорганизую большой проект WCF, который требует некоторых (в основном) статических данных, кэшированных и заполненных из базы данных SQL. Он должен истекать и «обновляться» не реже одного раза в день, поэтому я использую MemoryCache.Безопасность памяти 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; 
    } 
} 
+0

ObjectCache является поточно, Я не думаю, что ваш класс может потерпеть неудачу. http://msdn.microsoft.com/en-us/library/system.runtime.caching.objectcache(v=vs.110).aspx Возможно, вы одновременно отправляетесь в базу данных, но это будет использовать только больше CPU чем это необходимо. –

+0

Хотя ObjectCache является потокобезопасным, его реализация может и не быть. Таким образом, вопрос MemoryCache. – Haney

ответ

34

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

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

+1

David, Просто для подтверждения, в самом простом классе класса, который у меня выше, вызов .Remove() на самом деле является потокобезопасным, если другой поток находится в процессе вызова Get()? Полагаю, я должен просто использовать отражатель и копать глубже, но там много противоречивой информации. –

+7

Это потокобезопасный, но подверженный условиям гонки ... Если ваш Get происходит до вашего удаления, данные будут возвращены в Get. Если сначала произойдет удаление, это не произойдет. Это очень похоже на грязное чтение в базе данных. – Haney

7

проверить эту ссылку: http://msdn.microsoft.com/en-us/library/system.runtime.caching.memorycache(v=vs.110).aspx

Перейти к самой нижней части страницы (или найдите текст «Безопасность потока»).

Вы увидите:

^Тема безопасности

Этот тип поточно.

+4

Я прекратил доверять определению MSDN «Thread Safe» сам по себе довольно давно, опираясь на личный опыт. Вот хороший текст: [ссылка] (http://stackoverflow.com/questions/3137931/msdn-what-is-thread-safety) –

+2

Это сообщение немного отличается от ссылки, приведенной выше. Различие очень важно в том, что ссылка, которую я предоставил, не предлагала никаких предостережений для объявления безопасности потоков.У меня также есть личный опыт использования «MemoryCache.Default» в очень большом объеме (миллионы кеш-запросов в минуту) без проблем с потоками. – EkoostikMartin

10

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

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

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

4

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

Чтобы указать, что Reed Copsey заявляет в своем замечательном 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()); 

Чтобы выполнить это все асинхронно, я рекомендую использовать Stephen Toub's отличной AsyncLazy<T> реализации найденной в его article на 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()); 
Смежные вопросы