Using ReaderWriterLock part 3 (last in this set)
Well I think it's raining ReaderWriterLocks this month! Jeff Richter has an article on MSDN that goes over a lot of the basics and some stuff that's a lot more than basic. And then there's Vance Morrison's March article on the subject as well. Lots of good stuff in both of those.
And rounding things out, there's my little series of brain teasers -- part 1 and part 2.
I've got a good number of comments with regards to the simple problem I originally posed in part 1 and the more complicated one involving a database in part 2. Thanks to all that posted or wrote!
There's some recurring themes in the comments and they tend to speak to my five original questions so I'll try to summarize them here:
- ReaderWriterLock will have higher raw costs than Monitor
- The amount of work done inside the lock makes a huge difference in deciding which type of concurrency control you can afford
- The number of threads of each type (reader/writer) tends to matter more than the frequency of entry
- Sometimes the whole locking situation can be finessed by exposing read-only objects, however,
- The amount of data that is being shared limits the ability to finesse locking issues by creating read-only objects
- And last but not least ReaderWriterLock can be potentially upgraded
In part 2 I answered some of the original 5 questions (keeping in mind as usual I only give approximately correct answers because complete answers would require an entire book) but here they all are again with reference to the above.
#1 Is this a good use of ReaderWriterLock? What assumptions do you have to make about the frequency of the operations.
The original code can be easily changed into an example where the write becomes atomic and the read only data is in some sort of immutable object. So it's not an especially good candidate for a ReaderWriterLock.
#2 If UpdateUsefully were the method that was called nearly always would you give the same answer?
If that were the case then really we're blocked on the write side of the reader writer lock all the time and so we're just using it as an expensive Monitor. There's not much reason for it.
#3 What if ComputeSomethingUseful were called almost exclusively instead, does that change your answer?
Here we're basically saying that read threads are very rare. If that were the case we may as well just use a Monitor (i.e. standard lock) and forget the whole thing.
#4 Is there a different approach to solve this particular problem that might be more robust generally?
Several solutions involving immutable state were offered. All of those are an excellent approach that often generalizes. I offered an example where there is too much state to readily clone.
#5 What "tiny" change could I make in this problem that would make ReaderWriterLock virtually essential?
Create a situation where the lock must be upgraded sometimes (e.g. if the result is greater than 100 then reset).
I'll leave you with one more variation on the original theme. If we must create a lock with guaranteed upgrade to writer semantics (i.e. I say that I want to be upgradeable and I want to be the next writer for sure if I choose to upgrade) how would we build that?
It might look something like this (although you could do it more effectively by building the functionality directly into ReaderWriterLock --for big units of work it won't matter much anyway).
public class MixedUsers
{
private static Object myLock = new Object();
private static System.Threading.ReaderWriterLock rw =
new System.Threading.ReaderWriterLock();
private static int val1 = 0;
private static int val2 = 0;
// this method is called by many threads
public static int ComputeSomethingUseful()
{
// disregarding timeout effects for now
rw.AcquireReaderLock(-1);
int result = val1 + val2;
rw.ReleaseReaderLock();
return result;
}
// this method is called by many threads
public static int ComputeSomethingUsefulMaybeUpgrade(int v)
{
// note this does not block other readers
lock (myLock)
{
// should we acquire the readerlock here?
// is it necessary?
// is it a good idea anyway?
// is it a bad idea?
int result = val1 + val2;
if (result > v)
{
// disregarding timeout effects for now
// this effectively waits for all readers to exit
// we are the only writer because we own myLock
// we could use UpgradeToWriterLock instead of myLock
// if we didn't care that another writer got in before us,
// but then we'd have to revalidate val1 and val2
rw.AcquireWriterLock(-1);
val1 = 0;
val2 = 0;
result = 0;
rw.ReleaseWriterLock();
return result;
}
// release the readerlock here if you think it must be acquired above
return result;
}
}
// this method is called by many threads
public static int UpdateUsefully(int v1, int v2)
{
lock (myLock)
{
rw.AcquireWriterLock(-1); // looks redundant but is it?
val1 += v1; // note coordination of updates
val2 += v2; // these two sums need to happen atomically
int result = val1 + val2;
rw.ReleaseWriterLock();
return result;
}
}
}
There's some final code and I'll leave the analysis as an exercise to my readers. But I will say off hand that this kind of synchronization housekeeping is probably a bad idea given the very small amount of work we are synchronizing. So it's best to pretend that the body of the methods is meatier or really it's all moot.