Main menu

Post navigation

The FactoryDictionary

In answering this StackOverflow question, it got me thinking a little more about managing locks and multithreaded interaction with something like a Dictionary<T,T>. While I’m happy with the answer that I produced, I thought I would turn it into something a little more formal and discuss it here for a bit. This afternoon I put together a small class that I’m calling FactoryDictionary. While it’s not intended to be a pure implementation of a factory pattern, I felt the name was appropriate since the class itself is deciding whether or not to create a new instance.

The purpose of the FactoryDictionary is to be able to provide a thread-safe (in that operations are synchronized) implementation of a Dictionary<T,T> that also incorporated the ability to block the calling thread if a new instance needed to be created for a particular key (or if an instance was in the process of being created for a particular key) without blocking threads that were retrieving values for other keys. This is primarily targeted at heavyweight objects that are created infrequently and retrieved frequently. The approach of the FactoryDictionary involves offsetting the heavy instantiation process into an internal wrapper class that uses a user-supplied delegate to create the object. In doing so, the lock on the internal dictionary (used for retrieving instances of the wrapper class) can be released relatively quickly, but the calling thread can still be blocked by establishing a lock on an instance-specific synchronization object.

The majority of the code in the FactoryDictionary is a boilerplate implementation of the IDictionary<TKey, TValue> interface (and the other associated interfaces), which I won’t go into here since they’re fairly straightforward. Using these functions, the FactoryDictionary operates in the same manner as the Dictionary<TKey, TValue> class. The segment we’re actually interested in is contained in two parts: the ValueWrapper class and the GetOrCreate function. Relevant code appears below:

The implementation is pretty simple: the dictionary inspects its internal dictionary to see if a wrapper exists for the specified key. If one does not, it creates a wrapper and supplied the user-specified delegate that will actually do the heavy lifting in creating the object. Once the wrapper is in hand, it calls WaitForInitialization to pause, if necessary, for object instantiation. The delegate approach was taken to support objects with parameterized constructors; while I could have specified the new() generic condition on the TValue argument, that would only allow objects with a parameterless constructor to be stored.

Upon the first call to WaitForInitialization, an exclusive lock is acquired on the synchronization object, the creation flag is set, the object is created, the synchronization object is cleared, and the exclusive lock is released. If a subsequent call occurs before the object is created (or, more specifically, before the synchronization object is cleared), the thread blocks until prior locks are released. Once the locked code block is entered, the creation flag has already been set so no action is performed and the lock is immediately released. Calls that occur after the object has been created will return immediately, since the synchronization object has been cleared.

Taking this approach yield fairly transparent code, and consumption of the FactoryDictionary is simple. Consider a dictionary with a string key and a heavyweight object value type named HeavyweightObject.

FactoryDictionary<string, HeavyweightObject> dictionary;

All that is required to use the pseudo-double-checking dictionary functionality is the following:

dictionary.GetOrCreate("key", ()=>new HeavyweightObject());

This will either retrieve or create the object associated with the “key” string, using the lambda-declared delegate to perform the actual instantiation. In this way, subsequent calls to GetOrCreate with “key” as the key will only block (rather than instantiate a throw-away instance) and calls for other keys will not be affected.

A full source code download is available in my corresponding CodeProject article, or you can download it directly from me below.