Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does MemoryCache throw NullReferenceException

Tags:

Update

See updates below, issue is fixed the moment you install .Net 4.6.


I want to implement something within the UpdateCallback of CacheItemPolicy.

If I do so and test my code running multiple threads on the same cache instance (MemoryCache.Default), I'm getting the following exception when calling the cache.Set method.

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# 

I know that MemoryCache is thread safe so I didn't expect any issues. More importantly, if I do not specify the UpdateCallback, everything works just fine!

Ok, for reproducing the behavior, here we go with some console app: This code is just a simplified version of some tests I'm doing for another library. It is meant to cause collisions within a multithreaded environment, e.g. getting a condition where one thread tries to read a Key/Value while another thread already deleted it etc...

Again, this should all work fine because MemoryCache is thread safe (but it doesn't).

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

You should directly get the exception if you run that. If you comment out the UpdateCallback setter, you should not get an exception anymore. Also if you run only one thread (change Enumerable.Repeat<Action>(() => TestRun(), 10) to , 1)), it will work just fine.

What I found so far:

I found that whenever you set the Update or Remove callback, MemoryCache will create an additional sentinel cache entry for you with keys like OnUpdateSentinel<your key>. It seems that it also creates a change monitor on that item, because for sliding expiration, only this sentinel item will get the timeout set! And if this item expires, the callback will get invoked.

My best guess would be that there is an issue within MemoryCache if you try to create the same item with the same key/policy/callback at roughly the same time, if we define the Callback...

Also as you can see from the stacktrace, the error appears somewhere within the Dispose method of the ChangeMonitor. I didn't add any change monitors to the CacheItemPolicy so it seems to be something controlled internally...

If this is correct, maybe this is a bug in MemoryCache. I usually cannot believe finding bugs in those libraries because usually it is my fault :p, maybe I'm just too stupid to implement this correctly... So, any help or hints would be greatly appreciated ;)

Update Aug. 2014:

Seems they try to fix this issue.

Update May 2015:

Looks like the issue is fixed if you install e.g. the VS 2015 RC which comes with .Net 4.6. I cannot really verify which version of .Net fixes it because now it works in all versions the project uses. Doesn't matter if I set it to .Net 4.5, 4.5.1 or 4.5.2, the error is not reproduceable anymore.

like image 235
MichaC Avatar asked Feb 10 '14 14:02

MichaC


People also ask

Should I dispose MemoryCache?

MemoryCache implements IDisposable so you should call Dispose before replacing the old instance.

When should I use MemoryCache?

An in-memory cache removes the performance delays when an application built on a disk-based database must retrieve data from a disk before processing. Reading data from memory is faster than from the disk. In-memory caching avoids latency and improves online application performance.

Is MemoryCache a singleton?

Note that the MemoryCache is a singleton, but within the process. It is not (yet) a DistributedCache. Also note that Caching is Complex(tm) and that thousands of pages have been written about caching by smart people. This is a blog post as part of a series, so use your head and do your research.

How does MemoryCache work C#?

In-Memory Cache is used for when you want to implement cache in a single process. When the process dies, the cache dies with it. If you're running the same process on several servers, you will have a separate cache for each server. Persistent in-process Cache is when you back up your cache outside of process memory.


1 Answers

It would seem that Microsoft has fixed this, at least in .Net 4.5.2. Browsing referencesource.microsoft.com shows that there's now a lock around the access to the dictionary they're using to store internal data:

MemoryCacheEntry.cs

    internal void RemoveDependent(MemoryCacheEntryChangeMonitor dependent) {         lock (this) {             if (_fields._dependents != null) {                 _fields._dependents.Remove(dependent);             }         }     } 
like image 197
antiduh Avatar answered Oct 21 '22 12:10

antiduh