【问题标题】:Why does MemoryCache throw NullReferenceException为什么 MemoryCache 会抛出 NullReferenceException
【发布时间】:2014-03-07 23:23:16
【问题描述】:

更新

请参阅下面的更新,安装 .Net 4.6 后问题即已解决。


我想在CacheItemPolicyUpdateCallback 中实现一些东西。

如果我这样做并测试我的代码在同一个缓存实例 (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);
    }

}

如果你运行它,你应该直接得到异常。如果您注释掉 UpdateCallback 设置器,则不应再出现异常。此外,如果您只运行一个线程(将Enumerable.Repeat&lt;Action&gt;(() =&gt; TestRun(), 10) 更改为, 1)),它会正常工作。

到目前为止我发现了什么:

我发现,每当您设置 UpdateRemove 回调时,MemoryCache 都会为您创建一个额外的 sentinel 缓存条目,其中包含 OnUpdateSentinel&lt;your key&gt; 之类的键。似乎它还在那个项目上创建了一个更改监视器,因为对于滑动到期,只有这个哨兵项目会得到超时设置!如果此项过期,回调将被调用。

如果我们定义回调,我最好的猜测是MemoryCache 中存在问题,如果您尝试使用相同的键/策略/回调在大致相同的时间创建相同的项目...

从堆栈跟踪中也可以看出,错误出现在 ChangeMonitor 的 Dispose 方法中的某处。我没有向CacheItemPolicy 添加任何更改监视器,所以它似乎是内部控制的......

如果这是正确的,这可能是 MemoryCache 中的一个错误。我通常不敢相信在这些库中发现错误,因为通常这是我的错:p,也许我太愚蠢了,无法正确实现它......所以,任何帮助或提示将不胜感激;)

2014 年 8 月更新:

他们似乎试图修复this issue

2015 年 5 月更新:

如果您安装例如,问题似乎已解决。 .Net 4.6 附带的 VS 2015 RC。我无法真正验证哪个版本的 .Net 修复了它,因为现在它适用于项目使用的所有版本。无论我将其设置为 .Net 4.5、4.5.1 还是 4.5.2,都无法再重现该错误。

【问题讨论】:

标签: c# .net multithreading caching .net-4.5


【解决方案1】:

微软似乎已经解决了这个问题,至少在 .Net 4.5.2 中是这样。浏览referencesource.microsoft.com 表明现在对他们用来存储内部数据的字典的访问权限被锁定:

MemoryCacheEntry.cs

    internal void RemoveDependent(MemoryCacheEntryChangeMonitor dependent) {
        lock (this) {
            if (_fields._dependents != null) {
                _fields._dependents.Remove(dependent);
            }
        }
    }

【讨论】:

    【解决方案2】:

    我通过 NullReferenceException 找到了这个线程 在内存缓存中。 当我尝试向缓存添加内容时,我的问题是收到 NullReferenceException。

    NullReferenceException
       at System.Runtime.Caching.MemoryCacheStore.UpdateExpAndUsage(MemoryCacheEntry entry, Boolean updatePerfCounters)
       at System.Runtime.Caching.MemoryCacheStore.AddOrGetExisting(MemoryCacheKey key, MemoryCacheEntry entry)
       at System.Runtime.Caching.MemoryCache.AddOrGetExistingInternal(String key, Object value, CacheItemPolicy policy)
       at System.Runtime.Caching.ObjectCache.Add(String key, Object value, CacheItemPolicy policy, String regionName)
    

    MemoryCache 是线程安全的。 我们在静态字段中使用了一个对象。 NRE 的原因是另一个单独的线程试图通过调用 cache.Dispose() 来清除 MemoryCache;缓存 = 新的 MemoryCache(); 问题很容易在 2 个并行任务中重现: 一项任务将添加新对象 第二个将调用 Dispose 和新的 MemoryCache,就在 0.5 秒后,您将在某个地方收到 NRE,包括 MemoryCache .net 4.6.1

    我刚刚将 .Dispose 和 new MemoryCache 替换为 foreach(var kv in cache){ cache.remove(kv.key) }

    【讨论】:

      猜你喜欢
      • 2010-09-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-01-28
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多