There seem to be two criteria in this matter:
- "without loss of read lock after EndWrite
- "no other authors can execute while this block of code is executing
I will not consider the first point further, as others have already done so. However, the second point is very delicate and needs to be explained.
First of all, let me say what I mean Delphi 2007. I do not have access to 2009. However, it is unlikely that the behavior I am describing would change.
The code that you show does allows other writers to change the value during a block of code. When a read lock rises to a write lock, the read lock is temporarily lost. There is a point in time when your thread does not have a read or write lock. This is by design, because otherwise the dead end will be almost certain. If a thread that pushes a read lock to a write lock actually held a read lock at the same time, the following scenario could well happen:
- (thread 1) get read lock
- (stream 2) get read lock (normal, read blocked)
- (stream 1) get a write lock (blocks; stream 2 has a read lock)
- (stream 2) get a write lock (blocks; stream 1 has a read lock - now locked)
To prevent this, TMuliReadExclusiveWriteSynchronizer releases a read lock for some βinstantβ access until it receives a write lock.
(Note: the article Working with TMultiReadExclusiveWriteSynchronizer on EDN, in the section βBlock it Chris, I'm going to ....β It seems incorrect to assume that the script I just mentioned would actually be a dead end. It could have been written about the previous one version of Delphi, otherwise it might just be wrong. Or I might not understand what it requires. However, look at some of the comments on the article.)
So, assuming nothing more about the context, the code you showed is almost certainly incorrect. Checking the value when you have a read lock, and then pushing it into a write lock and assuming that the value has not changed, is an error. This is a very subtle catch with TMuliReadExclusiveWriteSynchronizer.
Here are some separate parts of the comments in the Delphi library code:
Other threads have the ability to modify the protected resource when you call BeginWrite before you are granted a write lock, even if you already have a read lock. The best policy is not to save any information about a protected resource (for example, account or size) through write a lock. Always retrieve protected resource samples after you obtain or release a write lock. The result of the BeginWrite function indicates whether another thread has received a write lock while the current thread is waiting for a write lock. A return value of True means that a write lock was obtained without any intermediate modifications to other threads. A return value of False means that the other thread received a write lock while you were waiting, so the resource protected by the MREWS object should be considered changed. Any protected resource samples should be discarded. In general, it is best to always retrieve the protected resource samples after receiving a write lock. The BeginWrite Boolean result and RevisionLevel properties help in cases where re-sampling is expensive or time consuming.
Here is some code to try. Create a global TMultiReadExclusiveWriteSynchronizer named Lock. Create two global booleans: Bad and GlobalB. Then run one instance of each of these threads and check the Bad value from the 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 not deterministic, you will probably see very quickly (less than 1 second) that the Bad value is set to True. So basically you see that the GlobalB value is True, and then when you check it a second time, it is False, although both checks occurred between the BeginRead / EndRead pair (and the reason is that there was also a pair inside BeginWrite / EndWrite).
My personal advice: never push read lock to write lock. It is too easy to misunderstand. In any case, you never push read locks to write locks (because you temporarily lose read locks), so you can also make this explicit in your code by simply calling EndRead before BeginWrite. And yes, that means you will need to check the status inside BeginWrite again. Therefore, in the case of the code that you showed initially, I would not even freeze the record. Just start with BeginWrite because it may decide to write.