为什么MemoryCache会抛出NullReferenceException异常

23

更新

请看下面的更新,只要安装了.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,一切都运行得很好!

好的,为了重现这个问题,我们来看一些控制台应用程序代码: 这段代码只是另一个库的一些测试的简化版本。它旨在在多线程环境下引起冲突,例如当一个线程尝试读取Key/Value时,另一个线程已经删除它等等...

再次强调,这应该都能正常工作,因为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<Action>(() => TestRun(), 10) 更改为 , 1)),则可以正常工作。

我发现,每当设置 UpdateRemove 回调时,MemoryCache 将为您创建一个额外的名称为 OnUpdateSentinel<your key> 的“标记”缓存条目。看起来它还会在该项上创建更改监视器,因为对于滑动过期,只有这个标记项才会设置超时!如果此项过期,将调用回调函数。

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

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

如果是这样,那么可能这是 MemoryCache 中的 bug。通常,我无法相信在这些库中发现错误,因为通常是我的错 :p,也许我实现得不正确...因此,任何帮助或提示都将不胜感激;)

2014 年 8 月更新:

看起来他们试图修复这个问题

2015 年 5 月更新:

如果您安装了例如 VS 2015 RC,则该问题应该已经解决了,其中包括 .Net 4.6。我无法真正验证哪个版本的 .Net 修复了它,因为现在它在项目使用的所有版本中都可以工作。无论我将其设置为 .Net 4.5、4.5.1 还是 4.5.2,都无法再次重现该错误。


3
解决方案是不要使用它 ^^. 对于滑动过期,最终我也会使用绝对过期,在获取时手动更新元素的到期时间。这样它就能正常工作了... 见https://cachemanager.codeplex.com/SourceControl/latest#src/CacheManager.SystemRuntimeCaching/MemoryCacheHandle`1.cs方法'GetCacheItemInternal' - MichaC
1
根据@MichaC添加的链接,问题已解决...在这里https://connect.microsoft.com/VisualStudio/feedback/details/817211/memorycache-throw-nullreferenceexception-if-cacheitempolicy-updatecallback-is-defined#tabs - Juan
我在Win8上不断地收到异常,但在Server 2012 R2上,在10分钟后仍然没有异常。 - Chris F Carroll
@antiduh - 它们都在CLR v4.0.30319上都是4.5.x版本。由System.Runtime.Caching.dll报告的程序集名称/版本为4.0.0.0。我可以就这个“something”再回复你。 - Chris F Carroll
我收回之前的说法。那个可行的程序已经安装了4.6版本的框架。 - Chris F Carroll
显示剩余3条评论
2个回答

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);
            }
        }
    }

0
我在内存缓存中通过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(); cache = new MemoryCache();来清除MemoryCache。 问题很容易在只有2个并行任务的情况下重现: 一个任务将添加新对象 第二个任务将在0.5秒后调用Dispose和new MemoryCache,您将在MemoryCache内的某个地方收到NRE。 .net 4.6.1

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接