Tuesday, March 31, 2009

Generic thread-safe access to Cache

In my last post I showed how we needed to move the log4net logger to Cache and use a lock to avoid multiple loads of the config file. Well we had some other places in the code that also stored objects in Cache so I wanted to repeat the same pattern. Using a generic method and a delegate I was able to create the following function that can safely access the Cache from multiple threads:
/// <summary>
/// Delegate to a method that returns a new instance of the object 
/// if to be used if its not found in the Cache.
/// </summary>
public delegate object CacheLoaderDelegate();

/// <summary>
/// Return a typed object from Cache. This method locks the creation of a 
/// new instance to provide thread safety.
/// </summary>
/// <typeparam name="T">Type of object to return.</typeparam>
/// <param name="key">The key used to reference the object.</param>
/// <param name="expirationMinutes">The interval between the time the
/// inserted object is last accessed and the time at which that object expires. 
/// If this value is the equivalent of 20 minutes, the object will expire and 
/// be removed from the cache 20 minutes after it was last accessed.</param>
/// <param name="loaderDelegate">Delegate to a method that returns a new 
/// instance of the object if to be used if its not found in the Cache.</param>
/// <returns>Instance of T</returns>
public static T GetObject<T>(string key, int expirationMinutes, CacheLoaderDelegate loaderDelegate) {
  Cache webCache = HttpRuntime.Cache;
  object value = webCache[key];

  // If the value is null create a new instance and add it to the cache.
  if (value == null) {

    // The reason for the lock and double null check is explained in this article
    // http://aspalliance.com/1705_A_New_Approach_to_HttpRuntimeCache_Management.all

    // It shows a coding flaw where multiple threads are checking the cache 
    // concurrently. Each of them discovers the object missing from cache and
    // attempts to create and insert it. Because thread 1 has not inserted the 
    // object before thread 2 checks for it, they both believe it’s missing and both 
    // create it. The solution is to use a mutex that prevents multiple threads from 
    // simultaneously entering the section of code that populates the cache.
    lock (typeof(CacheUtils)) {
      value = webCache[key];
      if (value == null) {
        // Call loader delegate to a get a new instance.
        value = loaderDelegate();

        // Insert the new object in the Cache for a sliding number of minutes. 
        // This means if the ConfigurationManager is not accessed in that time period 
        // it will be removed from cache. Also see notes in the class comments.
        webCache.Insert(key, value, null, Cache.NoAbsoluteExpiration, 
        TimeSpan.FromMinutes(expirationMinutes));
      }
    }
  } 

  // Verify that value (whether from the cache or the delegate is of the requested type.
  if (!(value is T)) {
    string message = string.Format("The value in the cache or returned by the loader delegate does not match the requested type: {0}", typeof(T));
    throw new InvalidCastException(message);
  }

  // Return the requested type.
  return (T)value;
}

No comments: