简体   繁体   中英

Microsoft Enterprise Library Caching Application Block not thread safe?

I created a super simple console app to test out the Enterprise Library Caching Application Block, and the behavior is baffling. I'm hoping I screwed something that's easy to fix in the setup. I have each item expire after 5 seconds for testing purposes.

Basic setup -- "Every second pick a number between 0 and 2. If the cache doesn't already have it, put it in there -- otherwise just grab it from the cache. Do this inside a LOCK statement to ensure thread safety.

APP.CONFIG:

<configuration>
  <configSections>
    <section name="cachingConfiguration" type="Microsoft.Practices.EnterpriseLibrary.Caching.Configuration.CacheManagerSettings, Microsoft.Practices.EnterpriseLibrary.Caching, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
  </configSections>
  <cachingConfiguration defaultCacheManager="Cache Manager">
    <cacheManagers>
      <add expirationPollFrequencyInSeconds="1" maximumElementsInCacheBeforeScavenging="1000"
      numberToRemoveWhenScavenging="10" backingStoreName="Null Storage"
      type="Microsoft.Practices.EnterpriseLibrary.Caching.CacheManager, Microsoft.Practices.EnterpriseLibrary.Caching, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
      name="Cache Manager" />
    </cacheManagers>
    <backingStores>
      <add encryptionProviderName="" type="Microsoft.Practices.EnterpriseLibrary.Caching.BackingStoreImplementations.NullBackingStore, Microsoft.Practices.EnterpriseLibrary.Caching, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
      name="Null Storage" />
    </backingStores>
  </cachingConfiguration>
</configuration>

C#:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Practices.EnterpriseLibrary.Common;
using Microsoft.Practices.EnterpriseLibrary.Caching;
using Microsoft.Practices.EnterpriseLibrary.Caching.Expirations;

namespace ConsoleApplication1
{
    class Program
    {
        public static ICacheManager cache = CacheFactory.GetCacheManager("Cache Manager");
    static void Main(string[] args)
        {
            while (true)
            {
                System.Threading.Thread.Sleep(1000); // sleep for one second.
                var key = new Random().Next(3).ToString();
                string value;
                lock (cache)
                {
                    if (!cache.Contains(key))
                    {
                        cache.Add(key, key, CacheItemPriority.Normal, null, new SlidingTime(TimeSpan.FromSeconds(5)));
                    }
                    value = (string)cache.GetData(key);
                }
                Console.WriteLine("{0} --> '{1}'", key, value);
                //if (null == value) throw new Exception(); 
            }
        }
    }
}

OUTPUT -- How can I prevent the cache from returning nulls?

2 --> '2'
1 --> '1'
2 --> '2'
0 --> '0'
2 --> '2'
0 --> '0'
1 --> ''
0 --> '0'
1 --> '1'
2 --> ''
0 --> '0'
2 --> '2'
0 --> '0'
1 --> ''
2 --> '2'
1 --> '1'
Press any key to continue . . .

What you are seeing is that your CacheItem has expired due to the 5 second SlidingTime expiration.

Before returning the cached value, the GetData method performs a check to see if the CacheItem has expired. If it has expired, the CacheItem is removed from the cache and null is returned. However, the call to Contains will return true because the CacheItem is in the cache even though it's expiration may have elapsed. This seems to be by design. With that in mind, it would be wise not to cache a null value to represent no data since you would not be able to discern an expired CacheItem from an actual cached value of null.

Assuming that you do not cache a null value then Luke's solution should suit you:

value = cache.GetData(key) as string;

// If null was returned then it means that there was no item in the cache 
// or that there was an item in the cache but it had expired 
// (and was removed from the cache)
if (value == null)
{
    value = key;
    cache.Add(key, value, CacheItemPriority.Normal, null,
        new SlidingTime(TimeSpan.FromSeconds(5)));
}


See The Definitive Guide To Microsoft Enterprise Library for more information.

I notice that you seem to be getting null back from the cache whenever that item hasn't been accessed during the previous 5 loop iterations (ie, 5 seconds). Could this be related to your 5 second expiry time?

It seems unlikely, but maybe you have a race condition and the items are dropping out of the cache between the Contains check and the GetData retrieval.

Try this change and see if it makes any difference to the output:

while (true)
{
    System.Threading.Thread.Sleep(1000);

    var key = new Random().Next(3).ToString();
    string value;

    lock (cache)
    {
        value = (string)cache.GetData(key);
        if (value == null)
        {
            value = key;
            cache.Add(key, value, CacheItemPriority.Normal, null,
                new SlidingTime(TimeSpan.FromSeconds(5)));
        }
    }
    Console.WriteLine("{0} --> '{1}'", key, value);
}

One of the reasons that .Contains can come back as true and .GetData can return a null is that .GetData goes through the whole expiration system (it seems to only return data which isn't expired) and .Contains doesn't check to see if it's content is expired.

{
    cache.Add("key", "value", CacheItemPriority.Normal, 
              null, new SlidingTime(TimeSpan.FromSeconds(5)));
    System.Threading.Thread.Sleep(6000);
    Console.WriteLine(cache.Contains("key"));        /// true
    Console.WriteLine(cache.GetData("key") != null); /// false
    Console.WriteLine(cache.Contains("key"));        /// false
}

Another problem I had was that I couldn't tell whether the cache contained an entry with null as the value or the cache just didn't contain an entry for key. A workaround that I use is that if .GetData comes back with a null and .Contains is true , then a null was purposefully stored in the cache and is not expired.

Although this might not fix your particular problem, double-checked locking is usually suggested...

if (!cache.Contains(key))
{
    lock(mylockobj)
    {
        if (!cache.Contains(key))
        {
             cache.Add(key, key)
        }
    }
}

Also possibly look into CacheItemRemovedCallback.

A bit old, however I faced a very similar issue today in that if I retrieve a value from the cache that is due to expire (plus up to 25 extra seconds) I receive a null value. However Microsoft have acknwoledged this situation and suggested a fix here , I just have to figure out how to implement it.

I know this question is quite old, but the issue is that you're using Contains then attempting to retrieve the value. In between the time Contains and GetData were called, the item expired and was removed, so GetData returned null. This is known as a race condition.

The solution is quite simple (without using locks), don't use Contains

private static void CachingBlockTest()
{
    while (true)
    {
        System.Threading.Thread.Sleep(2000);

        var key = new Random().Next(3).ToString();
        string value = cache.GetData(key) as string;

        if (value == null)
        {
            value = key;
            cache.Add(key, value, CacheItemPriority.Normal, new RefreshAction(),
                new SlidingTime(TimeSpan.FromSeconds(5)));
        }
        Console.WriteLine("{0} --> '{1}'", key, value);
    } 
}
private class RefreshAction : ICacheItemRefreshAction
{
    public void Refresh(string removedKey, object expiredValue, CacheItemRemovedReason removalReason)
    {
        Console.WriteLine("{0} --> {1} removed", removedKey, expiredValue);
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM