Joris Dresselaers

I want to know God's thoughts... the rest are details. - Albert Einstein

Thread-safe caching mechanism using a Hashtable

Hi. I wanted to start my first (real) blog post with some interesting material ;-)

On our current project we have been struggling with the following problem. We need a custom caching system that uses a static Hashtable to cache its items. As we work in a multi-threaded environment, this solutions needs to be thread safe. Which was off-course the difficulty.

Down the road we bumped into a few possibilities:

1. Using the lock() mechanism Visual studio provides

This was not a good solution as the creation of the instance was happening within the lock() statement. This had a really bad influence on performance. All threads would be in a waiting state whenever a new instance was created. So we moved on.

 

2. Using the ReaderWriterLockSlim class

This class is new in the .NET Framework 3.5 and is an improvement on the ReaderWriterLock class in previous versions of the framework. This young man wrote quite a good blog post on this topic. And this guy also came up with a thread safe Dictionary implementation (easily convertible into a Hashtable).

This solution was still no good as we were in need for a finer grained locking mechanism. This means that a lock can only occur when 2 threads are asking for the same object. When ThreadA is waiting on object X to be created, and ThreadB is also asking for object X and ThreadC asks for an instance of object Y, there may not be a waiting period for ThreadC. ThreadB though will always have to wait until ThreadA has completed its work and will then get a reference to the same instance created by ThreadA.

Close, but no cigar. We moved on again. 

 

3. The solution

After brainstorming with a few colleagues, we came up with a solution.

Instead of maintaining one Hashtable with the instances, we will maintain 2 Hashtables: one for the instances and one for the storage of locking objects. This way, we would only have to lock the instanceLocks Hashtable whenever we would instantiate a new object. The instance Hashtable would remain untouched during the creation period. The Instance Hashtable would still get locked afterward, but only to add the newly created instances to its collection. You might also recognize the double-check locking pattern to make sure the object being locked didn't change while locking it.

Voila, we found what we were looking for. Below is the code to accomplish this.

By the way: if you are wondering why the delegate is declared in the namespace; we did this so we can pass the method - which we call to actually create the object - to the Get method. This way, the creation of the objects is separated from the caching mechanism.

Hope this is of relevant use to you. Enjoy.

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;

namespace CustomCaching
{
public delegate TContract AnonymousDelegate<TContract>();

public class HashtableCache
{
private static Hashtable instances = new Hashtable();
private static Hashtable instanceLocks = new Hashtable();

public TContract Get<TContract>(AnonymousDelegate<TContract> anonymousBuildUpDelegate)
where TContract : class
{
string key = typeof(TContract).FullName;
TContract contract = default(TContract);

// See if the instance already exists, without taking a lock on it.
contract = TryGetInstance<TContract>(key);
if (contract != null)
{
return contract;
}

object instanceLock;

// The instance wasn't found on the first try. Now we take a lock on the hashtable.
lock (instances)
{
// Some other thread might have accessed the hashtable already,
// so do a second check.

contract = TryGetInstance<TContract>(key);
if (contract != null)
{
return contract;
}

// We still haven't found the instance, so let's create it.
// We keep a dictionary of creation locks per key;
// this is our fine grained locking mechanism.
lock (instanceLocks)
{
// If the lock is already created by another thread, use it. If not
// create one ourselves and add it to the locks hashtable.
if (instanceLocks.ContainsKey(key))
{
instanceLock = instanceLocks[key];
}
else
{
instanceLock = new object();
instanceLocks.Add(key, instanceLock);
}
}
}

// Now we take a lock on the finer grained object.
lock (instanceLock)
{
// Check again if the singleton was already created. If so return it,
// after removing the creation lock.
contract = TryGetInstance<TContract>(key);
if (contract != null)
{
RemoveInstanceLock(key);
return contract;
}

// We can now call the provided delegate to create our contract for us.
contract = anonymousBuildUpDelegate();

// Take a lock on the instances hashtable and add the created instance.
lock (instances)
{
// Double check: if the key does exist,
// something has gone wrong.

if (instances.ContainsKey(key))
{
throw new ApplicationException(
string
.Format("A duplicate key {0} exists in the hashtable.",
key));
}
instances.Add(key, contract);
}

// We remove the lock object from the locks hashtable
RemoveInstanceLock(key);

return contract;
}
}

private static void RemoveInstanceLock(string key)
{
// Remove the locking object from the dictionary if it exists.
lock (instanceLocks)
{
instanceLocks.Remove(key);
}
}

private static T TryGetInstance<T>(string key) where T : class
{
if (instances.ContainsKey(key))
{
return (T)instances[key];
}
else
{
return null;
}
}
}
}
Posted: Feb 15 2008, 09:26 AM by Jokke | with 1 comment(s)
Filed under: ,

Comments

Nexus said:

At first i would say, thanks.

But then again i am a pain in the ass, make your privates static readonly and you are trying to lock the hashtable itself

lock (instances)            {

and second else                    {                        instanceLock = new object();

This requires a lock all by itself, never initialize the syncroot object except for in your private

private static readonly object syncroot = new object();

Because .net compiler threats those as if they were public variables.

Greets

# February 29, 2008 5:08 AM
Leave a Comment

(required) 

(required) 

(optional)

(required) 


Enter the numbers above: