Reputation: 13380
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.
Upvotes: 23
Views: 6317
Reputation: 2638
I found this thread by NullReferenceException in memory cache. My problem is receiving NullReferenceException when i was trying to add something to cache.
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 is thread safe. We used one object in static field. Reason for NRE was one of other separate thread was trying to clear MemoryCache by calling cache.Dispose(); cache = new MemoryCache(); problem is easy to reproduce in just 2 parallel tasks: one task will add new objects second one will call Dispose and new MemoryCache, just after 0.5 second you will receive NRE somewhere incide MemoryCache .net 4.6.1
i just replaced .Dispose and new MemoryCache with
foreach(var kv in cache){ cache.remove(kv.key) }
Upvotes: 0
Reputation: 12425
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:
internal void RemoveDependent(MemoryCacheEntryChangeMonitor dependent) {
lock (this) {
if (_fields._dependents != null) {
_fields._dependents.Remove(dependent);
}
}
}
Upvotes: 1