Почему MemoryCache выбрасывает исключение NullReferenceException
Обновление
См. обновления ниже, проблема исправлена с момента установки .Net 4.6.
Я хочу что-то реализовать в UpdateCallback
CacheItemPolicy
.
Если я это сделаю и протестирую свой код с несколькими потоками в одном экземпляре кеша (MemoryCache.Default
), я получаю следующее исключение при вызове метода cache.Set
.
System.Runtime.Caching.dll!System.Runtime.Caching.MemoryCacheEntry.RemoveDependent(System.Runtime.Caching.MemoryCacheEntryChangeMonitor dependent = {unknown}) C#
System.Runtime.Caching.dll!System.Runtime.Caching.MemoryCacheEntryChangeMonitor.Dispose(bool disposing = {unknown}) C#
System.Runtime.Caching.dll!System.Runtime.Caching.ChangeMonitor.DisposeHelper() C#
System.Runtime.Caching.dll!System.Runtime.Caching.ChangeMonitor.Dispose() C#
System.Runtime.Caching.dll!System.Runtime.Caching.ChangeMonitor.InitializationComplete() C#
System.Runtime.Caching.dll!System.Runtime.Caching.MemoryCacheEntryChangeMonitor.InitDisposableMembers(System.Runtime.Caching.MemoryCache cache = {unknown}) C#
System.Runtime.Caching.dll!System.Runtime.Caching.MemoryCacheEntryChangeMonitor..ctor(System.Collections.ObjectModel.ReadOnlyCollection<string> keys = {unknown}, string regionName = {unknown}, System.Runtime.Caching.MemoryCache cache = {unknown}) C#
System.Runtime.Caching.dll!System.Runtime.Caching.MemoryCache.CreateCacheEntryChangeMonitor(System.Collections.Generic.IEnumerable<string> keys = {unknown}, string regionName = {unknown}) C#
System.Runtime.Caching.dll!System.Runtime.Caching.MemoryCache.Set(string key = {unknown}, object value = {unknown}, System.Collections.ObjectModel.Collection<System.Runtime.Caching.ChangeMonitor> changeMonitors = {unknown}, System.DateTimeOffset absoluteExpiration = {unknown}, System.TimeSpan slidingExpiration = {unknown}, System.Runtime.Caching.CacheEntryUpdateCallback onUpdateCallback = {unknown}) C#
System.Runtime.Caching.dll!System.Runtime.Caching.MemoryCache.Set(string key = {unknown}, object value = {unknown}, System.Runtime.Caching.CacheItemPolicy policy = {unknown}, string regionName = {unknown}) C#
Я знаю, что MemoryCache
является потокобезопасным, поэтому я не ожидал никаких проблем. Что еще более важно, если я не укажу UpdateCallback
, все работает отлично!
Хорошо, для воспроизведения поведения здесь мы переходим к консольному приложению:
Этот код является просто упрощенной версией некоторых тестов, которые я делаю для другой библиотеки. Он предназначен для возникновения столкновений в многопоточной среде, например. получение условия, когда один поток пытается прочитать ключ/значение, а другой поток уже удалил его и т.д.
Опять же, все должно работать нормально, потому что MemoryCache является потокобезопасным (но это не так).
class Program
{
static void Main(string[] args)
{
var threads = new List<Thread>();
foreach (Action action in Enumerable.Repeat<Action>(() => TestRun(), 10))
{
threads.Add(new Thread(new ThreadStart(action)));
}
threads.ForEach(p => p.Start());
threads.ForEach(p => p.Join());
Console.WriteLine("done");
Console.Read();
}
public static void TestRun()
{
var cache = new Cache("Cache");
var numItems = 200;
while (true)
{
try
{
for (int i = 0; i < numItems; i++)
{
cache.Put("key" + i, new byte[1024]);
}
for (int i = 0; i < numItems; i++)
{
var item = cache.Get("key" + i);
}
for (int i = 0; i < numItems; i++)
{
cache.Remove("key" + i);
}
Console.WriteLine("One iteration finished");
Thread.Sleep(0);
}
catch
{
throw;
}
}
}
}
public class Cache
{
private MemoryCache CacheRef = MemoryCache.Default;
private string InstanceKey = Guid.NewGuid().ToString();
public string Name { get; private set; }
public Cache(string name)
{
Name = name;
}
public void Put(string key, object value)
{
var policy = new CacheItemPolicy()
{
Priority = CacheItemPriority.Default,
SlidingExpiration = TimeSpan.FromMinutes(1),
UpdateCallback = new CacheEntryUpdateCallback(UpdateCallback)
};
MemoryCache.Default.Set(key, value, policy);
}
public static void UpdateCallback(CacheEntryUpdateArguments args)
{
}
public object Get(string key)
{
return MemoryCache.Default[ key];
}
public void Remove(string key)
{
MemoryCache.Default.Remove( key);
}
}
Вам следует получить исключение, если вы запустите это. Если вы прокомментируете Setter UpdateCallback, вы больше не должны получать исключение. Также, если вы запускаете только один поток (измените Enumerable.Repeat<Action>(() => TestRun(), 10)
на , 1)
), он будет работать нормально.
То, что я нашел до сих пор:
Я обнаружил, что всякий раз, когда вы устанавливаете обратный вызов Update
или Remove
, MemoryCache
создаст для вас дополнительную запись кэша кэш-памяти с такими ключами, как OnUpdateSentinel<your key>
. Похоже, что он также создает монитор изменений на этом элементе, потому что для истечения срока действия всего лишь этот контрольный элемент получит тайм-аут! И если этот элемент истекает, обратный вызов будет вызван.
Мое лучшее предположение заключается в том, что в MemoryCache
есть проблема, если вы попытаетесь создать тот же элемент с тем же ключом/политикой/обратным вызовом примерно в одно и то же время, если мы определим Callback...
Также, как вы можете видеть из stacktrace, ошибка появляется где-то внутри метода Dispose
ChangeMonitor. Я не добавлял никаких мониторов изменений в CacheItemPolicy
, поэтому, похоже, что-то контролируется внутренне...
Если это правильно, возможно, это ошибка в MemoryCache. Обычно я не могу поверить в обнаружение ошибок в этих библиотеках, потому что обычно это моя ошибка: p, может быть, я просто слишком глуп, чтобы реализовать это правильно... Итак, любая помощь или подсказки были бы очень благодарны;)
Обновление от 2014 года:
Кажется, они пытаются исправить эту проблему.
Обновление до 2015 года:
Похоже, проблема устранена, если вы устанавливаете, например, VS 2015 RC, который поставляется с .Net 4.6. Я не могу проверить, какая версия .Net исправляет его, потому что теперь он работает во всех версиях, которые использует проект. Не имеет значения, установил ли я его .Net 4.5, 4.5.1 или 4.5.2, ошибка больше не воспроизводится.
Ответы
Ответ 1
Казалось бы, Microsoft исправила это, по крайней мере, в .Net 4.5.2. Просмотр referencesource.microsoft.com показывает, что теперь есть блокировка доступа к словарю, который они используют для хранения внутренних данных:
MemoryCacheEntry.cs
internal void RemoveDependent(MemoryCacheEntryChangeMonitor dependent) {
lock (this) {
if (_fields._dependents != null) {
_fields._dependents.Remove(dependent);
}
}
}