Locking in the Miscellaneous Utility Library

Namespace: MiscUtil.Threading.
Types involved: SyncLock, OrderedLock, LockToken, LockOrderException, LockTimeoutException

These classes have evolved from ideas put forward by Jeffrey Richter and Ian Griffiths, and written up on one of my threading pages. My hope is that they'll continue to evolve, but there is a performance issue - extra features tend to chip away at performance, and while I don't tend to worry too much about performance, it's clearly an issue when it comes to something as basic as locking. (This is especially true as if people are worried about the cost of locking, they'll tend to try to do clever things to avoid it, which is always a bad idea.)

Basic locking

Instead of using the lock statement on an object reference, you need to create an instance of one of the lock classes (currently SyncLock and OrderedLock), and then call one of the Lock method overloads to acquire the lock. The Lock methods each return a LockToken. Calling Dispose on the returned LockToken releases the lock. The locks are re-entrant, just as with the normal lock statement, so if you acquire a lock twice and release it once, you still own the lock and need to release it again in order to fully release ownership.

Now, manually calling Dispose will work, but is error-prone. A much better idiom is to use the using statement in C# (or the Using statement in VB 8 or higher). Here's an example:

using MiscUtil.Threading;

class Example
{
    SyncLock padlock = new SyncLock();
    
    void Method1
    {
        using (padlock.Lock())
        {
            // Now own the padlock
        }
    }      
    
    void Method2
    {
        using (padlock.Lock())
        {
            // Now own the padlock
        }
    }
}

Now, the above is using SyncLock to give exactly the same semantics as the lock statement. The only advantage is that because the padlock variable is of type SyncLock, it's very obvious that it's there for locking and not for anything else.

The LockToken returned by the Lock method is a struct. It must be disposed in order to release the monitor. The reason for making this a struct rather than a class is that it then only takes up a bit of stack space for each lock operation, rather than needing to create a whole extra object each time, which would be nasty for performance.

Note that LockToken doesn't have any finalizer (or anything faking that). If you forget to call Dispose on the lock token, you're in trouble. You'll either get a deadlock or a timeout exception sooner or later. I'd argue this is actually better than getting a less deterministic deadlock due to the GC running a finalizer. (Possibly IDisposable should have been a "hard" contract requiring disposal in the first place; this pattern is violating the lax nature of IDisposable, but at least it's for good reasons.)

LockToken doesn't have anything else one can do with it other than call Dispose, so assuming the using statement is available to you, there should be very few situations where you actually need to declare your own variable to hold it - just let the compiler create an anonymous one for you as in the code above.

The Monitor property

You may wish to use methods from System.Threading.Monitor such as Wait and Pulse. Rather than copying all the methods from that class into SyncLock, a lock exposes the monitor that it internally acquires with the Monitor property. This should be used carefully, however - if you call Monitor.Enter or Monitor.Exit and pass in the monitor used by a lock, you could stop the lock from appearing to work properly. Use with care. Here's a sample snippet:

// ... or if you want to be able to wait/pulse the monitor
using (LockToken token = syncLock.Lock(10000))
{
    Monitor.Wait (syncLock.Monitor);
    // or
    Monitor.Pulse (syncLock.Monitor);
}

Design note: It's tempting to remove this property and just provide the Pulse, PulseAll and Wait methods instead. However, as more functionality may be included in future versions of the Monitor class itself, it would be tricky to keep SyncLock in step. The advantage, of course, would be that messing things up would be harder - the class itself would be more robust. If enough people think the property should be removed, I'm willing to reconsider; the decision is on a bit of a knife-edge as it is. Please mail me with your thoughts on this. (The change would be backwardly incompatible, of course - but very easy to fix up in client source code. The library itself currently isn't versioned, and nor is it likely to be in the near future. People are likely to take what they want either by using (and perhaps modifying) an existing version, or just including the relevant parts of the source into their own source tree.)

Naming locks

This is probably the most straightforward feature of SyncLock - locks can have names. They're read-only, and are specified in the various constructor overloads available. They can be handy when debugging, and appear in the messages of timeout exceptions when they're thrown.

Timeouts

Arguably, simplifying attempting to acquire a lock with a timeout was the principal reason for Jeffrey and Ian's work in the first place. Simply put, each lock attempt has a timeout associated with it. If the timeout period expires without the lock being acquired, a LockTimeoutException is thrown. Note that this doesn't necessarily mean there is a bug in your code - just occasionally, things holding locks will take a long time, even though it's wise to try to hold locks for as short a period as possible.

The timeout can be specified as a number of milliseconds, or a TimeSpan value. The value Timeout.Infinite may be used to specify that the thread should wait as long as it takes (possibly forever) to acquire the lock. (This is the normal behaviour of the lock statement.)

If no timeout is specified, the default timeout for the lock is used. Each lock may have its own default timeout specified in its constructor (as a number of milliseconds), and if no default timeout is specified, the value of the static SyncLock.DefaultDefaultTimeout property is taken as the default timeout at construction time. (Changes to DefaultDefaultTimeout after a lock has been created don't affect the default timeout for that lock.) The initial value of DefaultDefaultTimeout is Timeout.Infinite. This means that unless any timeouts are specified, the locks will behave much like "normal" .NET locks.

Here's an example showing the three overloads of Lock():

using MiscUtil.Threading;

class Example
{
    // Set the default timeout to 10 seconds
    SyncLock padlock = new SyncLock(10000);
    
    void Method1
    {
        // Use the default timeout
        using (padlock.Lock())
        {
        }
    }      
    
    void Method2
    {
        // Use a timeout of 30 seconds
        using (padlock.Lock(30000))
        {
        }
    }
    
    void Method3()
    {
        // Use a timeout of 1 minute
        using (padlock.Lock(new TimeSpan(0,1,0)))
        {
        }
    }
}

Deadlock prevention

As well as SyncLock, an OrderedLock class is provided. This allows the concept of one lock having another OrderedLock as an "inner lock". The rule used to avoid deadlock is simple: you can't acquire a lock when you already own the inner lock of that lock. Each lock only has one direct inner lock, but the "innerness" is transitive: if you have three locks, outer, middle and inner, with the inner lock of outer being middle and the inner lock of middle being inner, then inner is also considered an inner lock of outer. You can acquire an inner lock without first acquiring the outer lock (otherwise there'd be no point in having two locks) but you can't acquire the inner lock and then the outer lock. You can, however, acquire the outer lock, then the inner lock, then the outer lock again. This can't cause deadlock, so is allowed. Here are some sample sequences of ordering, using the three locks described above. (This is assuming that no locks are released, by the way - you can obviously acquire the inner lock, release it, and then acquire the outer lock.)

The inner lock can be set either using the InnerLock property, or using the SetInnerLock() method. The use of the latter is that it returns the lock you call it on (the outer lock, not the inner one). This allows code such as:

using MiscUtil.Threading;

class Example
{
    static OrderedLock inner = new OrderedLock("Inner");
    static OrderedLock outer = new OrderedLock("Outer").SetInnerLock(inner);
}

Note that this idiom only applies (in C# at least) to static field initializers; instance field initializers cannot reference the instance being created, unfortunately. Setting inner locks for instance variables should be done in the constructor, like this:

using MiscUtil.Threading;

class Example
{
    OrderedLock inner = new OrderedLock("Inner");
    OrderedLock outer = new OrderedLock("Outer");
    
    Example()
    {
        outer.InnerLock = inner;
    }
}

Valid sequence examples

Invalid sequence examples

OrderedLock verifies the lock ordering before it tries to call Monitor.TryEnter. If the acquisition of the lock violates the ordering rules, a LockOrderException is thrown. This exception always indicates a bug in your code, as you have violated the rules you've set yourself by describing the relationships between locks.

OrderedLock exposes another property, Owner, which indicates the thread which currently owns the lock, or null if the lock is not owned by anyone. Keeping track of this information is required in order to verify lock ordering, but incurs a performance penalty (see the performance section below for details), which is why it is not available

Warning: no release validation

OrderedLock does not ensure that locks are released in the order they are acquired. This would give a significant performance penalty without being useful for most of the time. When locks are used with the using statement (which is the expected use) it is impossible for the release order to end up as anything other than the reverse of the acquisition order.

Performance

Performance of locking is important. It's important because the more expensive it is, the more people will try to avoid it, attempting to be clever and usually writing code which is not thread-safe in the process. The Miscellaneous Utility locking types are not quite as "pure" as they might be if performance were not an issue - there are places where code is inlined rather than calling a separate method, for instance, purely for (tested) performance reasons.

Memory

SyncLock and OrderedLock are both reference types with a fairly small footprint (partly depending on the length of the name chosen). OrderedLock takes up slightly more memory than SyncLock, but unless you are likely to have an awful lot of long-lived locks, it's unlikely to be a significant problem.

LockToken is a value type which just contains a reference to its "parent" lock. The reason for it being a value type is so that it can be allocated on the stack in the typical use case, thereby removing any heap allocation and garbage collection penalty.

Speed

Speed of code like this is deeply dependent on the exact configuration of the computer used. So far, I only have results for the laptop I've been using to develop the code and my desktop at work. The laptop has a single Pentium-M single-core processor and fairly fast memory. The desktop is a P4; memory type unknown. TODO - provide more detailed specs. The tests are currently run against .NET 1.1. If you are able to provide more results, I'd be happy to include them on this page. Please run the performance unit tests from the source distribution, making sure you build the Release configuration, and mail me the output from the console. If you're using NUnit GUI, look at the Console.Out tab. (The Debug configuration is significantly slower, but I don't regard that as an issue, as the Release-built library should generally be used anyway.) The performance measurements below are factors compared with "native" locking. For example, a factor of 2 would mean that using the locking provided by the library took twice as long as using "native" locking (the lock statement).

DescriptionFactor
LaptopDesktopx64
Acquiring and releasing a previously unowned SyncLock 1.82 2.26 1.04
Acquiring and releasing a SyncLock which was previously owned by the current thread 1.66 2.12 1.06
Acquiring and releasing a previously unowned OrderedLock which has no inner locks 3.46 3.49 2.08
Acquiring and releasing an OrderedLock which was previously owned by the current thread and which has no inner locks 3.07 3.11 1.84
Acquiring and releasing a previously unowned OrderedLock which has two inner locks 3.68 3.93 2.20
Acquiring and releasing an OrderedLock which was previously owned by the current thread and which has two inner locks 3.31 3.32 1.96

As you can see, the performance is still reasonable, especially for SyncLock. Very few applications are likely to see a significant performance degredation due to using the locks, and I believe the benefits easily outweigh the slight performance loss. As an example of how fast acquiring locks is, the first test managed to acquire a billion locks in under 45 seconds on my laptop. That's over 22 million locks per second. Unless you're acquiring at least a couple of hundred thousand locks per second, the performance difference made by using these locks is going to be lost in the noise.


Back to the main MiscUtil page.