Я предполагаю, что в этом коде есть проблемы с параллелизмом:
const string CacheKey = "CacheKey";
static string GetCachedData()
{
string expensiveString =null;
if (MemoryCache.Default.Contains(CacheKey))
{
expensiveString = MemoryCache.Default[CacheKey] as string;
}
else
{
CacheItemPolicy cip = new CacheItemPolicy()
{
AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
};
expensiveString = SomeHeavyAndExpensiveCalculation();
MemoryCache.Default.Set(CacheKey, expensiveString, cip);
}
return expensiveString;
}
Причина проблемы параллелизма заключается в том, что несколько потоков могут получить нулевой ключ и затем попытаться вставить данные в кеш.
Каким будет самый короткий и самый чистый способ сделать этот код устойчивым к параллелизму? Мне нравится следовать хорошей схеме в коде, связанном с кешем. Ссылка на онлайн-статью будет большим подспорьем.
ОБНОВИТЬ:
Я придумал этот код на основе ответа @Scott Chamberlain. Может ли кто-нибудь найти в этом проблемы с производительностью или параллелизмом? Если это сработает, это сэкономит много строк кода и ошибок.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Caching;
namespace CachePoc
{
class Program
{
static object everoneUseThisLockObject4CacheXYZ = new object();
const string CacheXYZ = "CacheXYZ";
static object everoneUseThisLockObject4CacheABC = new object();
const string CacheABC = "CacheABC";
static void Main(string[] args)
{
string xyzData = MemoryCacheHelper.GetCachedData<string>(CacheXYZ, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
string abcData = MemoryCacheHelper.GetCachedData<string>(CacheABC, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
}
private static string SomeHeavyAndExpensiveXYZCalculation() {return "Expensive";}
private static string SomeHeavyAndExpensiveABCCalculation() {return "Expensive";}
public static class MemoryCacheHelper
{
public static T GetCachedData<T>(string cacheKey, object cacheLock, int cacheTimePolicyMinutes, Func<T> GetData)
where T : class
{
//Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
T cachedData = MemoryCache.Default.Get(cacheKey, null) as T;
if (cachedData != null)
{
return cachedData;
}
lock (cacheLock)
{
//Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
cachedData = MemoryCache.Default.Get(cacheKey, null) as T;
if (cachedData != null)
{
return cachedData;
}
//The value still did not exist so we now write it in to the cache.
CacheItemPolicy cip = new CacheItemPolicy()
{
AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(cacheTimePolicyMinutes))
};
cachedData = GetData();
MemoryCache.Default.Set(cacheKey, cachedData, cip);
return cachedData;
}
}
}
}
}
c#
.net
multithreading
memorycache
Аллан Сюй
источник
источник
ReaderWriterLockSlim
?ReaderWriterLockSlim
Но я бы также использовал эту технику, чтобы избегатьtry-finally
заявлений.Dictionary<string, object>
где ключ - это тот же ключ, который вы используете в своем,MemoryCache
а объект в словаре - это просто базовый объект, который вы используетеObject
. Однако, как говорится, я бы рекомендовал вам прочитать ответ Джона Ханна. Без надлежащего профилирования вы можете больше замедлить свою программу из-за блокировки, чем приSomeHeavyAndExpensiveCalculation()
запуске двух экземпляров и отбрасывании одного результата.Ответы:
Это моя вторая итерация кода. Поскольку
MemoryCache
это потокобезопасно, вам не нужно блокировать начальное чтение, вы можете просто прочитать, и если кеш возвращает значение null, выполните проверку блокировки, чтобы узнать, нужно ли вам создавать строку. Это значительно упрощает код.EDIT : приведенный ниже код не нужен, но я хотел оставить его, чтобы показать исходный метод. Это может быть полезно для будущих посетителей, которые используют другую коллекцию, которая имеет поточно-безопасное чтение, но не поточно-безопасную запись (почти все классы в
System.Collections
пространстве имен похожи на это).Вот как я бы это сделал, используя
ReaderWriterLockSlim
для защиты доступа. Вам нужно выполнить своего рода « двойную проверку блокировки », чтобы увидеть, создал ли кто-нибудь кэшированный элемент, пока мы ждем, чтобы снять блокировку.источник
MemoryCache
которые попадают в шансы, по крайней мере, одна из этих двух вещей была неправильной.Существует библиотека с открытым исходным кодом [отказ от ответственности: я написал]: LazyCache, который IMO покрывает ваши требования двумя строками кода:
Он имеет встроенную блокировку по умолчанию, поэтому кэшируемый метод будет выполняться только один раз при промахе кеша, и он использует лямбда, поэтому вы можете выполнить «получить или добавить» за один раз. По умолчанию скользящее истечение 20 минут.
Есть даже пакет NuGet ;)
источник
Я решил эту проблему, применив метод AddOrGetExisting в MemoryCache и используя отложенную инициализацию .
По сути, мой код выглядит примерно так:
В худшем случае вы создаете один и тот же
Lazy
объект дважды. Но это довольно тривиально. ИспользованиеAddOrGetExisting
гарантий того, что вы когда-либо получите только один экземплярLazy
объекта, и поэтому вы также гарантированно вызовете дорогостоящий метод инициализации только один раз.источник
SomeHeavyAndExpensiveCalculationThatResultsAString()
возникло исключение, оно застревает в кеше. Даже временные исключения будут кэшироваться с помощьюLazy<T>
: msdn.microsoft.com/en-us/library/vstudio/dd642331.aspxНа самом деле, вполне возможно, что это нормально, хотя с возможным улучшением.
Теперь, в общем, шаблон, в котором у нас есть несколько потоков, устанавливающих общее значение при первом использовании, чтобы не блокировать получаемое и устанавливаемое значение, может быть следующим:
Однако, учитывая, что
MemoryCache
записи могут быть выселены, тогда:MemoryCache
это неправильный подход.MemoryCache
является потокобезопасным с точки зрения доступа к этому объекту, так что здесь это не проблема.Обе эти возможности, конечно же, должны быть продуманы, хотя единственный раз, когда существуют два экземпляра одной и той же строки, может быть проблемой, если вы делаете очень определенные оптимизации, которые здесь не применяются *.
Итак, у нас остались возможности:
SomeHeavyAndExpensiveCalculation()
.SomeHeavyAndExpensiveCalculation()
.И решить это может быть сложно (действительно, из тех вещей, где лучше профилировать, чем предполагать, что вы сможете это решить). Однако здесь стоит учесть, что наиболее очевидные способы блокировки при вставке предотвращают все добавления в кеш, включая те, которые не связаны.
Это означает, что если бы у нас было 50 потоков, пытающихся установить 50 различных значений, тогда нам придется заставить все 50 потоков ждать друг друга, даже если они даже не собирались выполнять одинаковые вычисления.
Таким образом, вам, вероятно, лучше будет иметь код, который у вас есть, чем код, который избегает состояния гонки, и если условие гонки является проблемой, вам, скорее всего, придется обработать это где-то в другом месте или понадобится другой стратегия кэширования, чем та, которая удаляет старые записи †.
Единственное, что я бы изменил, так это то, что я бы заменил вызов на
Set()
один наAddOrGetExisting()
. Из вышесказанного должно быть ясно, что это, вероятно, не обязательно, но это позволит собирать только что полученный элемент, уменьшая общее использование памяти и обеспечивая более высокое соотношение коллекций низкого поколения к коллекциям высокого поколения.Так что да, вы можете использовать двойную блокировку для предотвращения параллелизма, но либо параллелизм на самом деле не проблема, либо вы неправильно сохраняете значения, либо двойная блокировка в хранилище не будет лучшим способом ее решения. ,
* Если вы знаете, что существует только одна из каждой строки из набора, вы можете оптимизировать сравнения на равенство, что является почти единственным случаем, когда две копии строки могут быть неправильными, а не просто неоптимальными, но вы бы хотели сделать очень разные типы кеширования, чтобы это имело смысл. Например, сортировка
XmlReader
делает внутренне.† Вполне вероятно, что либо тот, который хранится бесконечно, либо тот, который использует слабые ссылки, поэтому он будет исключать записи только в том случае, если они не используются.
источник
Чтобы избежать глобальной блокировки, вы можете использовать SingletonCache для реализации одной блокировки для каждого ключа без увеличения использования памяти (объекты блокировки удаляются, когда больше не используются ссылки, а получение / выпуск является потокобезопасным, гарантируя, что только 1 экземпляр когда-либо используется через сравнение и поменять местами).
Использование это выглядит так:
Код находится здесь, на GitHub: https://github.com/bitfaster/BitFaster.Caching.
Существует также реализация LRU, которая легче, чем MemoryCache, и имеет несколько преимуществ: более быстрое одновременное чтение и запись, ограниченный размер, отсутствие фонового потока, внутренние счетчики производительности и т. Д. (Отказ от ответственности, я написал это).
источник
Консольный пример из MemoryCache , «Как сохранить / получить простые объекты класса»
Вывод после запуска и нажатия Any keyкроме Esc:
Сохранение в кеш!
Получение из кеша!
Some1
Some2
источник
источник
Однако немного поздно ... Полная реализация:
Вот
getPageContent
подпись:А вот и
MemoryCacheWithPolicy
реализация:nlogger
простоnLog
объект для отслеживанияMemoryCacheWithPolicy
поведения. Я воссоздаю кеш памяти, если объект запроса (RequestQuery requestQuery
) изменяется через делегат (Func<TParameter, TResult> createCacheData
), или воссоздаю заново, когда скользящее или абсолютное время достигает своего предела. Обратите внимание, что все тоже асинхронно;)источник
Сложно выбрать, какой из них лучше; lock или ReaderWriterLockSlim. Вам нужна реальная статистика чисел чтения и записи, соотношений и т. Д.
Но если вы считаете, что использование «блокировки» - правильный путь. Тогда вот другое решение для разных нужд. Я также включаю в код решение Аллана Сюй. Потому что оба могут понадобиться для разных нужд.
Вот требования, которые побудили меня к этому решению:
Код:
источник