为什么MemoryCache会抛出NullReferenceException

Mic*_*haC 22 .net c# multithreading caching .net-4.5

更新

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


我想实现内部的东西UpdateCallbackCacheItemPolicy.

如果我这样做并测试我的代码在同一个缓存实例(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#
Run Code Online (Sandbox Code Playgroud)

我知道这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);
    }

}
Run Code Online (Sandbox Code Playgroud)

如果你运行它,你应该直接获得异常.如果您注释掉UpdateCallback setter,则不应再出现异常.此外,如果您只运行一个线程(更改Enumerable.Repeat<Action>(() => TestRun(), 10), 1)),它将正常工作.

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

我发现无论何时设置UpdateRemove回调,MemoryCache都会为你创建一个额外的标记缓存条目,如:OnUpdateSentinel<your key>.它似乎也在该项上创建了一个更改监视器,因为对于滑动过期,只有这个sentinel项才会获得超时设置!如果此项目到期,则将调用回调.

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

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

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

2014年8月更新:

似乎他们试图解决这个问题.

2015年5月更新:

如果安装例如.Net 4.6附带的VS 2015 RC,看起来问题已得到修复.我无法真正验证哪个版本的.Net修复它,因为现在它适用于项目使用的所有版本.如果我将其设置为.Net 4.5,4.5.1或4.5.2无关紧要,则错误不再可再现.

ant*_*duh 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);
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)