提升读锁定到写锁定时TMultiReadExclusiveWriteSynchronizer的行为

时间:2009-07-27 10:06:44

标签: delphi synchronization delphi-2009

如何实现这样的同步结构:

Lock.BeginRead
try
  if Changed then
    begin
    Lock.BeginWrite;
    try
      Update;
    finally
      Lock.EndWrite;
    end;
    // ... do some other stuff ...
    end;
finally
  Lock.EndRead;
end;

在EndWrite之后没有丢失读锁定,因此在执行此代码块时没有其他编写器可以执行。

在这种情况下,Delphi 2009的TMuliReadExclusiveWriteSynchronizer如何表现?

4 个答案:

答案 0 :(得分:6)

这个问题似乎有两个标准:

  • “在EndWrite之后不会丢失读锁定”
  • “执行此代码块时,没有其他编写者可以执行”

我不会进一步讨论第一点,因为其他人已经这样做了。然而,第二点非常微妙,需要解释。

首先,我要说的是我指的是Delphi 2007.我无法访问2009.但是我所描述的行为不太可能发生变化。

您显示的代码 使其他编写者可以在代码块中更改值。当读锁定被提升为写锁定时,读锁定暂时丢失。有一段时间你的线程既没有读锁也没有写锁。这是设计的,因为否则几乎可以肯定死锁。 如果正在促进对写锁定的读锁定的线程实际上在执行此操作时保持读锁定,则可能很容易发生以下情况:

  1. (主题1)获取读锁
  2. (线程2)获取读锁定(好,共享读锁)
  3. (线程1)获得写锁定(块;线程2具有读锁定)
  4. (线程2)获得写锁定(块;线程1具有读锁定 - 现在已死锁)
  5. 为了防止这种情况,TMuliReadExclusiveWriteSynchronizer在获得写锁定之前释放一些“即时”的读锁定。

    (旁注:EDN上的文章Working with TMultiReadExclusiveWriteSynchronizer,在“将其锁定克里斯,我即将......”一节中似乎错误地暗示我刚刚提到的场景实际上会陷入僵局。可能是关于Delphi的早期版本的,或者可能只是错了。或者我可能误解了它的主张。但是请看一下文章中的一些评论。)

    因此,不再假设有关上下文的更多信息,您所显示的代码几乎肯定是不正确的。在读取锁定时检查值,然后将其提升为写入锁定并假设值未更改是错误的。这是一个非常微妙的TMuliReadExclusiveWriteSynchronizer。

    以下是Delphi库代码中注释的一些选定部分:

      

    其他线程有机会修改受保护资源     当你在被授予写锁之前调用BeginWrite时,甚至     如果您已经打开了读锁定。最好的政策是不保留     有关受保护资源(例如计数或大小)的任何信息     写锁。始终重新获取受保护资源的样本     获取或释放写锁定。 BeginWrite的函数结果表明是否有另一个线程     当前线程正在等待写锁定时写入锁定。     返回值True表示没有获取写锁定     其他线程的任何干预修改。返回值为False     意味着另一个线程在你等待的时候得到了写锁定,所以     受MREWS对象保护的资源应被视为已修改。     应丢弃受保护资源的任何样本。     一般来说,最好始终重新获取受保护的样本     获取写锁定后的资源。 BeginWrite的布尔结果     和RevisionLevel属性有助于重新获取样本的情况     计算量大或耗时。

    这是一些尝试的代码。创建名为Lock的全局TMultiReadExclusiveWriteSynchronizer。创建两个全局布尔值:Bad和GlobalB。然后启动每个线程的一个实例,并从主程序线程监视Bad的值。

    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;
    

    虽然它是不确定的,但您可能会很快(不到1秒)看到值Bad设置为True。所以基本上你看到GlobalB的值是True,然后当你第二次检查它时它是False,即使在BeginRead / EndRead对之间发生了两次检查(原因是因为里面也有一个BeginWrite / EndWrite对)

    我的个人建议:只是从不提升对写锁定的读锁定。这很容易弄错。在任何情况下,你永远不会真正提升对写锁的读锁(因为你暂时失去了读锁),所以你也可以通过在BeginWrite之前调用EndRead在代码中明确它。是的,这意味着您必须在BeginWrite内再次检查条件。因此,对于您最初显示的代码,我甚至根本不打算使用读锁定。刚开始使用BeginWrite,因为可能决定写。

答案 1 :(得分:4)

第一:来自EndWrite的代码驻留在TSimpleRWSync中,后者是IReadWriteSync的轻量级实现,而TMultiReadExclusiveWriteSynchronizer则更为复杂。

第二:如果仍然有一些对EnterCriticalSection(FLock)的开放调用(如BeginRead中的那个),则EndWrite中对LeaveCriticalSection(FLock)的调用不释放锁。

这意味着您的代码示例非常有效,并且在您使用TSimpleRWSync实例或TMultiReadExclusiveWriteSynchronizer实例时应该按预期工作。

答案 2 :(得分:3)

我没有Delphi 2009,但我希望TMultiReadExclusiveWriteSynchronizer的工作方式没有变化。我认为这是一个适用于你的场景的正确结构,只有一句话:“BeginWrite”是一个返回布尔值的函数。确保在执行写操作之前检查其结果。

此外,在Delphi 2006中,TMultiReadExclusiveWriteSynchronizer类中包含许多开发人员注释以及一些调试代码。确保在使用之前先看一下实现。

另见:EDN上的Working with TMultiReadExclusiveWriteSynchronizer

答案 3 :(得分:0)

感谢Uwe Raabe和Tihauan的答案:

TMultiReadExclusiveWriteSynchronizer可以使用这种嵌套锁定结构。 EndWrite不会释放读锁定,因此很容易在一段时间内启动对写锁定的读锁定,然后在没有其他写入器干扰的情况下返回读锁定。