(1 item) |
|
(1 item) |
|
(5 items) |
|
(1 item) |
|
(1 item) |
|
(2 items) |
|
(2 items) |
|
(4 items) |
|
(1 item) |
|
(6 items) |
|
(2 items) |
|
(4 items) |
|
(1 item) |
|
(4 items) |
|
(2 items) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(2 items) |
|
(2 items) |
|
(5 items) |
|
(3 items) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(3 items) |
|
(1 item) |
|
(1 item) |
|
(2 items) |
|
(8 items) |
|
(2 items) |
|
(7 items) |
|
(2 items) |
|
(2 items) |
|
(1 item) |
|
(2 items) |
|
(1 item) |
|
(2 items) |
|
(4 items) |
|
(1 item) |
|
(5 items) |
|
(1 item) |
|
(3 items) |
|
(2 items) |
|
(2 items) |
|
(8 items) |
|
(7 items) |
|
(3 items) |
|
(7 items) |
|
(6 items) |
|
(1 item) |
|
(2 items) |
|
(5 items) |
|
(5 items) |
|
(7 items) |
|
(3 items) |
|
(7 items) |
|
(16 items) |
|
(10 items) |
|
(27 items) |
|
(15 items) |
|
(15 items) |
|
(13 items) |
|
(16 items) |
|
(15 items) |
Sebastien Lambla posted an article about why he prefers the ReaderWriterLock
over the good old
Monitor
that I blogged about
last month. As he says:
"Well most of the time what you really want is let several threads read, but only one thread only write. Oh, and you don't want to let someone write while you're reading, nor do you want anyone reading while you write."
This all sounds perfectly reasonable, but I don't think it's as simple as that.
The right choice of synchronization primitive will be determined not only by the pattern of access to your data, but
also the amount of contention. Even if your access patterns are as Sebastian describes - what I like to call "almost
read-only" access - you still might find that ReaderWriterLock
performs less well than
Monitor
.
This seems counterintuitive. The Monitor
forces threads to take it in turns, while the
ReaderWriterLock
only serializes access when writes occur. You could be forgiven for thinking that
the one that allows the most work to proceed in parallel will give you the best throughput.
In practice, you're only going to see better throughput if you really do have lots of threads trying to use the
resource simultaneously - ReaderWriterLock
only offers any benefit if there is a significant amount of
contention for the resources you are protecting. That won't always be the case - consider something like this:
private static ArrayList fooList; public Foo CreateFoo() { Foo f = new Foo(); lock (fooList) { fooList.Add(f); } return f; }
How long do you suppose we're going to spend holding the monitor in this code? Probably not all that long - you
would expect ArrayList.Add
to run pretty quickly. To answer the question fully, we'd need to know
things like how often CreateFoo
is called, and how long it takes to construct a Foo
, but
unless the answers are 'lots' and 'very little', the chances are that the vast majority of the time will be spent
outside of that lock
block. And this is not a particularly unusual example.
Assuming that all the uses of this particular object's monitor are equally short-lived, how often will two or more threads actually be in contention for it?
On a single-CPU system, it wouldn't be surprising if there was never any contention for the lock. Code that doesn't perform any blocking operations while holding the monitor (like the example above) will only encounter contention if the thread's quantum happens to expire while the lock is held, and the OS scheduler pre-empts it and then runs another thread which then tries to acquire the same object's monitor. This is going to be an extremely rare occurrence. (This is one reason why threading bugs tend to manifest themselves far less often on single-CPU systems than on multi-CPU machines.)
Even on a multi-CPU box, it is quite plausible that contention for this monitor will be the exception rather than the rule. It will depend on the pattern of access to the data, but unless the shared data structure is a hot spot being used by many threads most of the time, you may well find that the lock is acquired without contention the vast majority of the time. Indeed, that's a desirable goal for your locking strategies - lock contention is usually bad for performance.
So if a particular monitor is acquired without contention the vast majority of the time, what will changing over to a
ReaderWriterLock
achieve? As Sebastien points out, it makes your code more complex. It is also
likely to slow you down - the cost of acquiring a read lock on a ReaderWriterLock
without contention is
much higher than the cost of acquiring a monitor without contention. In my tests the monitor looks to be over 5 times
faster. So in cases where there is very rarely contention for the lock, the monitor is likely to work out much faster as
well as simpler.
Also, remember that most of the classes in the .NET Framework aren't written to be safe even for read-only
use - most of them are documented as being safe only for access on one thread at a time. In these cases, the
ReaderWriterLock
is of no use to you. Of course the DataSet
and
DataTable
are notable exceptions - both of these say the following under Thread Safety:
This type is safe for multithreaded read operations. You must synchronize any write operations
So you'd think these would be a case where ReaderWriterLock
could be used safely. (Maybe even
to good effect, if you're doing substantial amounts of work while holding the lock.) But even then you have to be
careful - it's not entirely clear what constitutes a 'read'. For example, consider DataTable.Select
- this
retrieves a filtered, sorted subset of what is in the table. It does not alter the data in the table, nor have any directly
observable side effects on the DataTable
's internal state. And yet Microsoft considers it to be a write
operation!
I only know that DataTable.Select
counts as a 'write' operation because I wrote some code that
assumed it was a read operation, and it fell over under heavy load on a multi-CPU machine - Microsoft informed me of
the method's non-obvious and undocumented categorisation as a 'write' method in the ensuing support incident. The
reason is that DataTable
updates its internal index cache when you call this method. Even though that's
an implementation detail, Microsoft still regards this as a write operation... (I'm not sure how we are supposed to know
that from the docs, but that's how it is.)
So, what's the best thing to do?
I believe that choosing ReaderWriterLock
as your preferred default is at best premature optimisation,
and in practice is likely to cause harm more often than it delivers any benefit. It complicates the code, and it might
slow you down. You should only consider using ReaderWriterLock
when you are seeing lots of
contention. (You can use the good old Windows Performance Monitor to see how much lock contention you're
getting. Working out exactly which lock(s) are suffering most is more of a challenge, but
this essay on
deadlock and contention debugging has a lot of useful tips.) And even then you should measure your performance
with both styles of locking, using as realistic a workload as you can get, to find out whether the benefits you are
hoping for are delivered in practice.