Here's a bit of code I extracted from my own special baby of an application. It allows me to lock on a specific "name", rather than having to use an object instance and the built in lock construct.
Usage
NamedLock<string> locker = new NamedLock<string>();
var url = "http://services.digg.com...";
using (locker.Lock(url))
{
//Do something synchronized
var xml = new XmlDocument();
xml.Load(url);
}
Why?
There are a few problems I've come across where synchronizing a particular "name" might be useful. One of the apps I work one 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.
The meat and potatoes
Have a look at the class itself. It's relatively small, and consists of three things:
- The primary class with
LockandUnlockfunctions - An internal
Tokenclass that implementsIDisposable - An internal
ReferenceCountclass
NamedLock 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 relatively simple:
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.
Conclusión
I can't take full credit for this little class. I'm basically dumb, and needed lots of help from Peter Bright. He's a jackass, but he knows it so it's OK for me to say that.
There are some other things I'd like to be able to do that I currently can't with this class. Primarily, I'd like ReaderWriterLock type functionality. That would greatly complexify things, though, and it performs perfectly well for me at the moment.
Comment (1)
I did some experimenting with generics/Dictionary stuff and it was really easy and useful. I think I will try out your code and see if I could make use of it.