How can I achieve a synchronization structure like that:
Lock.BeginRead
try
if Changed then
begin
Lock.BeginWrite;
try
Update;
finally
Lock.EndWrite;
end;
// ... do some other stuff ...
end;
finally
Lock.EndRead;
end;
without loosing the read lock after the EndWrite, so that no other writers can execute while this code block is executed.
How does Delphi 2009's TMuliReadExclusiveWriteSynchronizer behave in this case?
It seems there are two criteria wrapped up in this question:
I will not address the first point further since others have already done so. However the second point is very delicate and needs explanation.
First of all, let me say I am referring to Delphi 2007. I do not have access to 2009. However it is unlikely that the behavior I'm describing would have changed.
The code you shows does make it possible for other writers to change the value during the code block. When the read lock is promoted to a write lock, the read lock is temporarily lost. There is an instant of time when your thread has neither a read or write lock. This is by design, since otherwise deadlock would be almost certain. If the thread which is promoting a read lock to a write lock actually held the read lock while doing so, the following scenario could quite easily occur:
To prevent this, TMuliReadExclusiveWriteSynchronizer releases the read lock for some "instant" before obtaining the write lock.
(Side note: The article Working with TMultiReadExclusiveWriteSynchronizer on EDN, in the section "Lock it up Chris, I'm about to..." seems to incorrectly suggest that the scenario I just mentioned actually would deadlock. This could have been written about a prior version of Delphi or it might just be mistaken. Or I might be misunderstanding what it is claiming. However look at some of the comments on the article.)
So, not assuming anything more about the context, the code you have shown is almost certainly incorrect. Checking a value while you have a read lock, then promoting it to a write lock and assuming the value has not changed is a mistake. This is a very subtle catch with TMuliReadExclusiveWriteSynchronizer.
Here are a few selected parts of the comments in the Delphi library code:
Here is some code to try. Create a global TMultiReadExclusiveWriteSynchronizer named Lock. Create two global Booleans: Bad and GlobalB. Then start one instance of each of these threads and monitor the value of Bad from your main program thread.
Although it is non-deterministic, you will probably see very quickly (less than 1 second) that the value Bad gets set to True. So basically you see the value of GlobalB is True and then when you check it a second time it is False, even though both checks occurred between a BeginRead/EndRead pair (and the reason is because there was also a BeginWrite/EndWrite pair inside).
My personal advice: Just never promote a read lock to a write lock. It is way too easy to get it wrong. In any case, you never are really promoting a read lock to a write lock (because you temporarily lose the read lock), so you may as well make it explicit in the code by just calling EndRead before BeginWrite. And yes, that means that you'd have to check the condition again inside the BeginWrite. So in the case of the code you showed originally, I would not even bother with a read lock at all. Just start with BeginWrite because it may decide to write.
First: Your code from EndWrite resides in TSimpleRWSync, which is a lightweight implementation of IReadWriteSync, while TMultiReadExclusiveWriteSynchronizer is much more sophisticated.
Second: The call to LeaveCriticalSection(FLock) in EndWrite doesn't release the lock if there are still some open calls to EnterCriticalSection(FLock) (like the one in BeginRead).
This means your code example is quite valid and should work as expected wether you are using a TSimpleRWSync instance or a TMultiReadExclusiveWriteSynchronizer instance.
Thanks to the answers of Uwe Raabe and Tihauan:
TMultiReadExclusiveWriteSynchronizer works fine with such nested locking structures. The EndWrite does not realease the read lock, so it is easily possible to promoto a read-lock to a write-lock for a certain period of time and then to return to the read-lock without other writers interfering.
I don't have Delphi 2009 but I expect there were no changes in the way TMultiReadExclusiveWriteSynchronizer works. I think it is the right structure to use for your scenario with one remark: the "BeginWrite" is a function returning a Boolean. Make sure you check its result before doing the write operations.
Also, in Delphi 2006 the TMultiReadExclusiveWriteSynchronizer class has a lot of developer comments in it and also some debug code. Make sure you take a look at the implementation before using it.
See also: Working with TMultiReadExclusiveWriteSynchronizer on EDN