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:
- "without losing the read lock after the EndWrite"
- "no other writers can execute while this code block is executed"
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:
- (thread 1) get read lock
- (thread 2) get read lock (ok, read lock is shared)
- (thread 1) get write lock (blocks; thread 2 has a read lock)
- (thread 2) get write lock (blocks; thread 1 has a read lock--now deadlocked)
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:
Other threads have an opportunity to modify the protected resource
when you call BeginWrite before you are granted the write lock, even
if you already have a read lock open. Best policy is not to retain
any info about the protected resource (such as count or size) across a
write lock. Always reacquire samples of the protected resource after
acquiring or releasing a write lock. The function result of BeginWrite indicates whether another thread got
the write lock while the current thread was waiting for the write lock.
Return value of True means that the write lock was acquired without
any intervening modifications by other threads. Return value of False
means another thread got the write lock while you were waiting, so the
resource protected by the MREWS object should be considered modified.
Any samples of the protected resource should be discarded.
In general, it's better to just always reacquire samples of the protected
resource after obtaining a write lock. The boolean result of BeginWrite
and the RevisionLevel property help cases where reacquiring the samples
is computationally expensive or time consuming.
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.
type
TToggleThread = class(TThread)
protected
procedure Execute; override;
end;
TTestThread = class(TThread)
protected
procedure Execute; override;
end;
{ TToggleThread }
procedure TToggleThread.Execute;
begin
while not Terminated do
begin
Lock.BeginWrite;
try
GlobalB := not GlobalB;
finally
Lock.EndWrite;
end;
end;
end;
{ TTestThread }
procedure TTestThread.Execute;
begin
while not Terminated do
begin
Lock.BeginRead;
try
if GlobalB then
begin
Lock.BeginWrite;
try
if not GlobalB then
begin
Bad := True;
Break;
end;
finally
Lock.EndWrite;
end;
end;
finally
Lock.EndRead;
end;
end;
end;
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.
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
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.