代码之家  ›  专栏  ›  技术社区  ›  jpfollenius Rob Kennedy

将读锁升级为写锁时tmultireadexclusivewritesynchronizer的行为

  •  5
  • jpfollenius Rob Kennedy  · 技术社区  · 15 年前

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

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

    在结束写入后不释放读锁,这样在执行此代码块时就没有其他编写器可以执行。

    Delphi2009的tmuliReadExclusiveWriteSynchronizer在这种情况下是如何工作的?

    4 回复  |  直到 15 年前
        1
  •  6
  •   Chad N B    13 年前

    在这个问题中,似乎有两个标准:

    • “在结束写入后不丢失读取锁定”
    • “执行此代码块时,其他编写器无法执行”

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

    首先,让我说我指的是Delphi2007。我不能进入2009年。然而,我描述的行为不太可能改变。

    你显示的代码 使其他编写器可以在代码块期间更改值。当读锁升级为写锁时,读锁暂时丢失。当线程既没有读锁也没有写锁时,会有一个瞬间。这是设计的,否则死锁几乎是肯定的。 如果 将读锁升级为写锁的线程在执行此操作时实际上持有读锁,很容易发生以下情况:

    1. (线程1)获取读取锁
    2. (线程2)获取读取锁(好,读取锁是共享的)
    3. (线程1)获取写锁(块;线程2具有读取锁)
    4. (线程2)get write lock(块;线程1有一个read lock——现在死锁了)

    为了防止这种情况发生,tmuliReadExclusiveWriteSynchronizer在获取写锁之前释放了一些“即时”的读锁。

    (旁注:文章 Working with TMultiReadExclusiveWriteSynchronizer 在EDN上,在“锁定克里斯,我要……”一节中,似乎错误地暗示了我刚才提到的场景实际上会死锁。这可能是关于先前版本的Delphi的文章,也可能只是弄错了。或者我可能误解了它的说法。不过,看看文章中的一些评论。)

    因此,不需要再假设任何关于上下文的内容,您所显示的代码几乎肯定是不正确的。在有读锁的情况下检查一个值,然后将其升级为写锁,并假定该值没有更改,这是一个错误。这是TMuliReadExclusiveWriteSynchronizer非常微妙的捕获。

    下面是Delphi库代码中注释的几个选定部分:

    其他线程有机会修改受保护的资源 当您在被授予写锁之前调用BeginWrite时,甚至 如果已经打开了读锁。最好的政策是不保留 有关受保护资源(如计数或大小)的任何信息 写入锁。总是在之后重新获取受保护资源的示例 获取或释放写锁。BeginWrite的函数结果指示是否有其他线程 当前线程正在等待写锁时的写锁。 返回值为true表示获取写锁时没有 其他线程的任何中间修改。返回值为假 意味着另一个线程在您等待时获得了写锁,因此 受mrews对象保护的资源应视为已修改。 应丢弃受保护资源的任何示例。 一般来说,最好总是重新获取受保护的样本 获取写锁后的资源。BeginWrite的布尔结果 以及RevisionLevel属性帮助重新获取样本 计算成本高或耗时。

    这里有一些代码要尝试。创建名为Lock的全局tmultireadExclusiveWriteSynchronizer。创造两个全球性的繁荣:坏的和全球性的。然后启动每个线程的一个实例,并从主程序线程监视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的值是真的,然后当您再次检查它时,它是假的,即使这两个检查都发生在beginread/endread对之间(原因是因为其中也有beginwrite/endwrite对)。

    我的个人建议:只是 从未 将读锁升级为写锁。这太容易出错了。在任何情况下,您都不会真正将读锁升级为写锁(因为您暂时失去了读锁),所以您也可以在代码中通过在beginwrite之前调用endread将其明确化。是的,这意味着你必须再次检查贝金华内部的情况。因此,对于您最初显示的代码,我甚至不需要使用读锁。从beginwrite开始,因为它 可以 决定写作。

        2
  •  4
  •   Uwe Raabe    15 年前

    首先:您的endwrite代码驻留在tsimplerwsync中,它是IReadWriteSync的轻量级实现,而tmultireadExclusiveWriteSynchronizer则更加复杂。

    第二:如果仍然有一些对EnterCriticalSection(flock)的开放调用(如beginread中的调用),那么在endwrite中对leaveCriticalSection(flock)的调用不会释放锁。

    这意味着您的代码示例非常有效,无论您使用的是tsimplerwsync实例还是tmultireadexclusivewritesynchronizer实例,都应按预期工作。

        3
  •  3
  •   Tihauan    15 年前

    我没有Delphi2009,但我希望tmultireadExclusiveWriteSynchronizer的工作方式没有变化。我认为,在您的场景中使用“beginwrite”是一个返回布尔值的函数,这是正确的结构。确保在执行写操作之前检查其结果。

    此外,在Delphi2006中,tmultireadExclusiveWriteSynchronizer类中有许多开发人员注释,还有一些调试代码。在使用之前,请确保您查看了实现。

    参见: Working with TMultiReadExclusiveWriteSynchronizer 关于EDN

        4
  •  0
  •   jpfollenius Rob Kennedy    15 年前

    感谢Uwe Raabe和Tihauan的回答:

    tmultireadExclusiveWriteSynchronizer与此类嵌套锁定结构一起工作正常。endwrite并不能真正缓解读锁,因此很容易在一定时间内将读锁升级为写锁,然后在不受其他编写器干扰的情况下返回到读锁。