Using ReaderWriterLock part 2
Well the trouble with simple samples like the one I provided in part 1 is that well... they're too simple. Some of the improvements that you can make in them won't work generally. But nonetheless I think there's some interesting discussion possibilities.
Let's have a look at it again:
public class MixedUsers
{
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 UpdateUsefully(int v1, int v2)
{
rw.AcquireWriterLock(-1);
val1 += v1; // note coordination of updates
val2 += v2; // these two sums need to happen atomically
int result = val1 + val2;
rw.ReleaseWriterLock();
return result;
}
}
Simple enough but is this really doing what we want? Several people suggested that we could cache the result that was computed on update. That's a good idea in fact as we could do that write atomically; I won't really explore that much though because it's really quite specific to this problem. If we had other reader methods with different results we couldn't use that technique. But there is something similar we could do that does work more generally.
public class MixedUsers
{
private static Object myLock = new Object();
class MyState
{
int val1;
int val2;
MyState(int v1, int v2) { val1 = v1; val2 = v2; }
}
private static MyState state;
// this method is called by many threads
public static int ComputeSomethingUseful()
{
MyState s = state;
return s.val1 + s.val2;
}
// this method is called by many threads
public static int UpdateUsefully(int v1, int v2)
{
lock (myLock)
{
MyState sNew = new MyState(state.val1 + v1, state.val2 + v2);
state = sNew; // object write is atomic
return state.val1 + state.val2;
}
}
}
I think I like that better than the original but what about my 5 points? Can I always do this?
Well I don't think so... let me give you a different example
public class MixedUsers
{
private static System.Threading.ReaderWriterLock rw =
new System.Threading.ReaderWriterLock();
private static InMemoryDatabase m = InitializeTheDatabase();
// this method is called by many threads, regularly
public static int ComputeSomethingUseful(int param)
{
// disregarding timeout effects for now
rw.AcquireReaderLock(-1);
int result = OneSecondComputationFromData(m, param);
rw.ReleaseReaderLock();
return result;
}
// this method is called by one threads once per minute or so
public static void UpdateUsefully()
{
rw.AcquireWriterLock(-1);
ReadExternalDataAndUpdateDatabaseInTenSeconds(m);
rw.ReleaseWriterLock();
}
}
Now why is that last example such a clear winner? There are several factors. What if ReaderWriterLock is 20 times lower than a regular lock, is still the right choice?
Here are the original questions, I think they're still worth discussing but I'll give some information with regard to the original posting.
#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. But here's a question: does it matter how often it is entered or does it matter more how many threads are doing it?
What about the last three questions, have we addressed these at all?
#3 What if ComputeSomethingUseful were called almost exclusively instead, does that change your answer?
#4 Is there a different approach to solve this particular problem that might be more robust generally?
#5 What "tiny" change could I make in this problem that would make ReaderWriterLock virtually essential?
Comments
- Anonymous
May 15, 2006
Instead of
rw.AcquireWriterLock(-1);
ReadExternalDataAndUpdateDatabaseInTenSeconds(m);
rw.ReleaseWriterLock();
you could do
InMemoryDatabase mNew = ReadExternalDataAndCreateNewDatabaseInTenSeconds();
rw.AcquireWriterLock(-1);
m = mNew; // so fast...
rw.ReleaseWriterLock();
That way you're not blocking readers for ten seconds while you read the external data.
A prerequisite for this to work is that UpdateUsefully is the ONLY thing that writes to the function. If other things write then you'd have to grab a read lock initially and then upgrade it to a write lock once you'd parsed the data:
rw.AcquireReaderLock(-1);
InMemoryDatabase mNew = ReadExternalDataAndCreateNewDatabaseInTenSeconds();
rw.UpgradeToWriterLock(-1);
m = mNew; // so fast...
rw.ReleaseWriterLock();
The upgrade of a read lock to write lock is a very easy place to get deadlocked, though. - Anonymous
May 15, 2006
The trouble with that approach is that it would require two copies of the database in memory which could be very expensive. Could it still be done in 10 seconds? Well it's a hypothetical example anyway.
But I think the point is made: if the amount of shared data is very large then isolation via immutability becomes problematic.
Note that upgrading the lock is also problematic because it is possible that some other writer gets in before you upgrade your lock. Although that's an interesting direction to persue and its something I will talk about later.
Some reader-writer locks have the notion of an
"upgradeable read" which guarantees that no other writer will modify the structures until you are exit the lock. The CLR structure just has ordinary read semantics so it cannot (by itself) make this guarantee. - Anonymous
May 15, 2006
> The trouble with that approach is that it would require two copies of the database in memory which could be very expensive
A very good point. On the one hand we have a solution where only one copy of the database is needed, but where readers are blocked while the database is refreshed.
On the other hand we have a solution where readers are only blocked for the time it takes to swap the reference out, but the memory needs to be able to hold two copies of the database.
Which solution is preferable will depend entirely on things like how big the database is, how much memory there is lying around, what the consequences of delaying the readers are, etc. etc. There's no unilateral "better" choice here, it depends entirely on the application. - Anonymous
May 15, 2006
Well I think it's raining ReaderWriterLocks this month! Jeff Richter has an article on MSDN that...