When a developer wants to store similar items in a
collection, there are several approaches to accomplishing that. Let us look at
a basic "User" class, a collection and the "simple" way to
load this.
Listing 1
public class User
{
private int _UserId;
public int UserId
{
get { return _UserId; }
set { _UserId = value; }
}
private string _Username;
public string Username
{
get { return _Username; }
set { _Username = value; }
}
}
public class MyClass
{
static MyClass()
{}
private static Dictionary<string, User> _Users =
new Dictionary<string, User>();
public static Dictionary<string, User> Users
{
get { return _Users; }
}
}
So, how is this loaded with users? A basic pattern that many
developers are familiar with is:
Listing 2
string username = ... // set the username here
User user = MyClass.Users[ username ];
if ( user == null )
{
//load user
user = GetUser( username );
MyClass.Users.Add( username, user );
}
That does indeed retrieve a user and then adds it to the
collection. But what happens when you have many requests?
Apply the Singleton Pattern
How do we apply the Singleton Pattern to this? For a great review
of the Singleton Pattern, check out this site after finishing
reading this article. So, we will
use a double checking lock pattern. The reason for this is because we are
dealing with a collection as our instance variable rather than a single
variable. But it still leads us to several other problems. How do we make this
thread safe for all items added to our collection? Or for all access to this
collection? One suggestion would be to use a single "locker" object
for this.
Listing 3
public class MyClass
{
public static readonly object Locker = new object();
private static Dictionary<string, User> _Users =
new Dictionary<string, User>();
public static Dictionary<string, User> Users
{
get { return _Users; }
}
}
// later in the code...
string username = ... // set the username here
User user = MyClass.Users[ username ];
if ( user == null )
{
lock ( MyClass.Locker )
{
// check again for the user object
user = MyClass.Users[ username ];
if ( user == null )
{
//load user
user = GetUser( username );
MyClass.Users.Add( username, user );
}
}
}
There is a serious problem with this. For starters, the
collection is completely exposed. It can be accessed directly without locking. Our
pattern for loading the collection has several flaws. Let us just assume for
right now that this is the only place in the code where we are loading this
collection. For every new username that is requested, we are blocking the
entire collection. Under even small load, this can cause quite a bit of
deadlocking. So, how are we supposed to accomplish [1] protecting the
collection from extraneous access and [2] providing a framework to allow
multiple items to be placed in the collection without collisions?
SingleKeyLockBox<TPrimaryKey, TValue>
We have some problems to solve here. How do we protect our
collection while adjusting it? How do we handle collision management? Let us
examine a code snip that should help illustrate these areas.
Listing 4
string pkey; // this is the key - or in this case the username
User value; // the variable for the User object we are trying to retrieve
// this is the lock object for the _PrimaryLockbox collection
// it is the central collection to the SingleKeyLockBox
lock ( _Locker )
{
// check for the key
if ( _PrimaryLockbox.TryGetValue( pkey, out value ) )
return value;
}
// GetKeyLock manages an internal collection of locking objects
// one for each key - it isn't safe to use the key as a locking object
// it may not be an object that is safe to use for that, so always assume
// it is not
lock ( GetKeyLock( pkey.GetHashCode(), _monitorLocker, _monitorLockbox ) )
{
// use the global collection lock just for checking the
// collection again for this key value
lock ( _Locker )
{
//added if statement - this should be the more accessed path
if ( _PrimaryLockbox.TryGetValue( pkey, out value ) )
return value;
}
// attempt to load the item if it isn't here
// a more thorough explanation later.
bool isValid = TryLoadByPrimaryKey(pkey, out value);
if( isValid )
{
SetValueForPrimary(pkey, value);
}
return value;
}
The idea throughout this example is that locking the central
collection should be done as "tight" as possible. Only lock the
central collection for [1] checking for existence of a key, [2] placing
something in the central collection, [3] removing something from the central
collection. Considering the keyed item, we only want to lock when we need to
populate the central collection with data. This helps to ensure reads are not
blocked (what if we ever want to reload a key?).
The method SetValueForPrimary is refactored to provide for a
thread safe way of setting values in the collection.