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

Почему 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, ошибка больше не воспроизводится.

4b9b3361

Ответ 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);
            }
        }
    }