Behaviour of TMultiReadExclusiveWriteSynchronizer

2019-05-02 10:02发布

问题:

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?

回答1:

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:

  1. (thread 1) get read lock
  2. (thread 2) get read lock (ok, read lock is shared)
  3. (thread 1) get write lock (blocks; thread 2 has a read lock)
  4. (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.



回答2:

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.



回答3:

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



回答4:

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.