TMultiReadExclusiveWriteSynchronizer behavior when promoting read lock to write lock

How can I achieve such a synchronization structure:

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 losing read locks after EndWrite, so no other authors can execute while this code block is executing.

How does Delphi 2009 TMuliReadExclusiveWriteSynchronizer work in this case?

+4
source share
4 answers

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.

+6
source

First: your EndWrite code is in TSimpleRWSync, which is an easy implementation of IReadWriteSync, while TMultiReadExclusiveWriteSynchronizer is much more complicated.

Second: calling LeaveCriticalSection (FLock) in EndWrite does not release the lock if there are any other open calls to EnterCriticalSection (FLock) (for example, in BeginRead).

This means that your sample code is valid and should work as expected if you are using an instance of TSimpleRWSync or an instance of TMultiReadExclusiveWriteSynchronizer.

+4
source

I do not have Delphi 2009, but I did not expect changes in the work of TMultiReadExclusiveWriteSynchronizer. I think this is the right structure for your script with one remark: "BeginWrite" is a function that returns a boolean value. Before performing write operations, make sure that you check the result.

In addition, in Delphi 2006, the TMultiReadExclusiveWriteSynchronizer class contains many developer comments, as well as some debugging code. Before use, make sure you review the implementation.

See also: Working with TMultiReadExclusiveWriteSynchronizer in EDN

+3
source

Thanks to the answers of Uwe Raabe and Tijahuan:

TMultiReadExclusiveWriteSynchronizer works great with such nested lock structures. EndWrite does not update the read lock, so you can easily push the read lock to write lock for a certain period of time, and then return to the read lock without the intervention of other authors.

0
source

All Articles