Introduction
There are a few problems I've come across where synchronizing a particular "name" might be useful. One of the apps I work on makes heavy use of Lucene.NET; each page shows the results of a couple of queries. It doesn't make a whole ton of sense to run multiple, identical queries against Lucene at the same time, so I generate a key for each query, lock against that key, and let the first thread do the actual work while the others sit around sipping coffee.
Using the code
It's pretty simple to use this class. Create a new instance of NamedLock<string>
, then call the Lock
function within a using
statement.
NamedLock<string> locker = new NamedLock<string>();
var url = "http://services.digg.com...";
using (locker.Lock(url))
{
var xml = new XmlDocument();
xml.Load(url);
}
Points of interest
Have a look at the class itself. It's relatively small, and consists of three things:
- The primary class with the
Lock
and Unlock
functions - An internal
Token
class that implements IDisposable
- An internal
ReferenceCount
class
NamedLock<t />
is pretty simple. It contains a Dictionary<T, ReferenceCount>
to keep track of which names are currently locked, and provides utility functions for acquiring and releasing locks. Lock
looks like this:
public IDisposable Lock(T name, int timeout)
{
Monitor.Enter(lockCollection);
ReferenceCount obj = null;
lockCollection.TryGetValue(name, out obj);
if (obj == null)
{
obj = new ReferenceCount();
Monitor.Enter(obj);
lockCollection.Add(name, obj);
Monitor.Exit(lockCollection);
}
else
{
obj.AddRef();
Monitor.Exit(lockCollection);
if (!Monitor.TryEnter(obj, timeout))
{
throw new TimeoutException(
string.Format(
"Timeout while waiting for lock on {0}",
name)
);
}
}
return new Token<T>(this, name);
}
This function locks the lockCollection
, checks for an existing lock with the same name, adds one if it's the first, then locks and returns a token. There's a good reason it uses Monitor.Enter
instead of a simple lock
statement: you'll notice that if there's no current lock in the collection, we actually lock the sync object (named obj
) before releasing the lock on the collection. If the lock does exist in the collection, we increment a reference counter, release the collection lock, and then lock on the sync object. Doing it this way lets us avoid deadlocks on the lock collection (bad juju).
The Unlock
function is also straightforward:
public void Unlock(T name)
{
lock (lockCollection)
{
ReferenceCount obj = null;
lockCollection.TryGetValue(name, out obj);
if (obj != null)
{
Monitor.Exit(obj);
if (0 == obj.Release())
{
lockCollection.Remove(name);
}
}
}
}
It locks the lockCollection
, grabs the sync object, releases the named lock, then removes the sync object if there aren't any other threads holding references to it. This code is a bit more straightforward, since we don't have to do anything janky to avoid dead locks on the lock collection.
The token class is so shockingly simple I'm not even going to paste it here. It takes a reference to the parent NamedLock
, and then calls parent.Unlock(name)
when disposed.
History