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.)
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.
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.)
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.
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))) { } } } |
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; } } |
outer middle inner
middle inner
outer inner
outer middle inner middle outer
outer middle outer inner middle
middle
inner middle
inner outer
middle outer
outer inner middle
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
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 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.
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 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).
Description | Factor | ||
---|---|---|---|
Laptop | Desktop | x64 | |
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.