更快的 TMultiReadExclusiveWriteSynchronizer?

Posted

技术标签:

【中文标题】更快的 TMultiReadExclusiveWriteSynchronizer?【英文标题】:Faster TMultiReadExclusiveWriteSynchronizer? 【发布时间】:2012-04-30 03:49:50 【问题描述】:

有没有一种更快TMultiReadExclusiveWriteSynchronizer?也许是 FastCode?​​p>

从 Windows Vista 开始,Microsoft 添加了Slim Reader/Writer lock。它performs much better 比德尔福的TMultiReadExclusiveWriteSynchronizer。不幸的是,它只存在于 Windows Vista 及更高版本中,实际上很少有客户使用。

大概Slim Reader/Writer lock 中使用的概念可以在本机 Delphi 代码中重做 - 但有人做过吗?

我有一种情况,在TMultiReadExclusiveWriteSynchronizer 上获取和释放锁(即使没有争用 - 单个线程),会导致 100% 的开销(操作时间加倍)。我可以在没有锁定的情况下运行,但是我的类不再是线程安全的。

有更快的TMultiReadExclusiveWriteSynchronizer吗?

注意:如果我使用TCriticalSection,我只会遭受 2% 的性能损失(尽管已知关键部分在 acquire 成功时会很快,即它是单线程的,没有争用)。 CS 的缺点是我失去了“多个阅读器”的能力。

测量结果

使用TMultiReadExclusiveWriteSynchronizer 会在BeginReadEndRead 中花费大量时间:

然后我移植代码以使用 Window 自己的 SlimReaderWriter Lock(一些代码重写,因为它不支持递归锁定获取),并分析了结果:

TMultiReadExclusiveWriteSynchronizer:每次迭代 10,698 ns 10,697,772,613 ns 迭代 1,000,000 次

SRWLock:每次迭代 8,802 ns 8,801,678,339 ns 迭代 1,000,000 次

Omni Reader-Writer lock:每次迭代 8,941 ns 8,940,552,487 ns 迭代 1,000,000 次

使用 SRWLocks(又名 Omni 的旋转锁)时提高了 17%。

现在,我无法将代码永久切换为使用Windows Vista SRWLocks,因为有一些完整的客户企业仍在使用 Windows XP。

Slim 锁只是对InterlockedCompareExchange 函数的谨慎使用;但比我能成功使用的要小心。我这个远不是仅仅窃取所涉及的 140 条机器指令,并且已经完成了。

阅读奖励

Is Critical Section always faster? How to write fast multi-thread Delphi applications

【问题讨论】:

@DavidHeffernan 哦,我无意自己写这样的东西。基于包含$IFDEF调试行的RTL源代码,自己写是不可能的。这就是为什么我正在寻找第 3 方实施。我不介意非本机 Delphi 代码 - 只要它可以编译成本机单个 Win32 可执行文件。 为什么这么少的企业客户还没有升级到 Windows 7 让您感到惊讶?我有一些客户希望他们能尽快将 SQL Server 数据库从 Windows 2000 Server 上移走。 我通常通过设计使锁定是不必要的来解决这个问题。我知道这听起来很虚伪,但我发现多线程有两种主要情况,我将它们命名为“响应式 UI”和“网络服务”。响应式 UI 通常使用消息队列来处理,并且设计合理的服务会话不需要锁定,因为给定的会话实际上是单线程的。我发现真正需要锁定的唯一情况是在网络服务中维护活动会话的集合。 由于您没有遇到争用延迟,因此您的所有锁定和解锁(为了避免争用)都是不必要的。只是保持锁定。您将获得更多争用(这很好),并且您将避免所有不必要的锁定/解锁,这些锁定/解锁会影响您的单线程性能。 (正如我们在商业中所说,您需要增加锁的范围。) 在不知道为什么需要这些锁的情况下很难推荐一种特定的技术。你的用例是什么? 【参考方案1】:

来自OmniThreadLibraryTOmniMREW声称更快更轻量级:

OTL 是一个出色的线程库,顺便说一句。

示例代码

TOmniReaderWriterLock = class(TInterfacedObject, IReaderWriterLock)
private
   omrewReference: Integer;
public
    IReaderWriterLock 
   procedure BeginRead;
   procedure EndRead;
   procedure BeginWrite;
   procedure EndWrite;
end;

 TOmniReaderWriterLock 

procedure TOmniReaderWriterLock.BeginRead;
var
  currentReference: Integer;
begin
    //Wait on writer to reset write flag so Reference.Bit0 must be 0 than increase Reference
    repeat
        currentReference := Integer(omrewReference) and not 1;
    until currentReference = Integer(InterlockedCompareExchange(Pointer(omrewReference), Pointer(Integer(currentReference) + 2), Pointer(currentReference)));
end;

procedure TOmniReaderWriterLock.EndRead;
begin
    //Decrease omrewReference
    InterlockedExchangeAdd(@omrewReference, -2);
end;

procedure TOmniReaderWriterLock.BeginWrite;
var
    currentReference: integer;
begin
    //Wait on writer to reset write flag so omrewReference.Bit0 must be 0 then set omrewReference.Bit0
    repeat
        currentReference := omrewReference and (not 1);
    until currentReference = Integer(InterlockedCompareExchange(Pointer(omrewReference), Pointer(currentReference+1), Pointer(currentReference)));

    //Now wait on all readers
    repeat
    until omrewReference = 1;
end;

procedure TOmniReaderWriterLock.EndWrite;
begin
    omrewReference := 0;
end;

【讨论】:

【参考方案2】:

最后我使用了一个折衷的解决方案。 Omni 读写锁使用 "slim" 原则(旋转位操作)。像 Window 自己的一样,它不支持锁升级。我已经对其进行了测试,它似乎不会lockup崩溃或死锁。

最后我使用了后备情况。支持“读写”概念的最通用的通用接口:

IReaderWriterLock = interface
   ['6C4150D0-7B13-446D-9D8E-866B66723320']
   procedure BeginRead;
   procedure EndRead;
   procedure BeginWrite;
   procedure EndWrite;
end;

然后我们在运行时决定使用哪个实现。如果我们使用的是 Windows Vista 或新版本,请使用 Window 自己的 SlimReaderWriter,否则回退到 Omni 版本:

TReaderWriterLock = class(TObject)
public
   class function Create: IReaderWriterLock;
end;

class function TReaderWriterLock.Create: IReaderWriterLock;
begin
   if Win32MajorVersion >= 6 then //SRWLocks were introduced with Vista/Server 2008 (Windows version 6.0)
   begin
      //Use the Windows built-in Slim ReaderWriter lock
      Result := TSlimReaderWriterLock.Create;
   end
   else
   begin
      //XP and earlier fallback to Omni equivalent
      Result := TOmniReaderWriterLock.Create;
   end;
end;

注意:任何代码都会发布到公共领域。无需署名。

【讨论】:

【参考方案3】:

Delphi TMultiReadExclusiveWriteSynchronizer 非常复杂 - 它可以递归获取,您可以从 Read 更新到 Write

这是有代价的,在这种情况下,这意味着为每个线程管理一个共享状态桶。由于 Windows 线程本地机制(可通过 threadvar 访问)对此过于简单(无法处理多个 MREWS 实例),它以一种相当低效的方式完成 - 请参阅 RTL 或 JCL 源 - 实现非常相似,共享不良性能和更新死锁风险。

首先确保您确实需要 MREWS 功能——我假设根据锁定开销与工作负载的比例大小,使用TCriticalSection 会更好。

如果您真的需要它,请使用 Delphi 实现并注意BeginWrite 中可能隐藏的解锁 - 请参阅它的文档和返回值含义。

可以使用Interlocked 函数或内联汇编来实现类似Vista 的SRW,但在大多数情况下不值得付出努力。

【讨论】:

真的可以将读取状态更新为 TMREWSync 的写入吗?【参考方案4】:

JCL 有一个 MREWS,它是一种不同的实现,可能对您有用。不确定它需要什么版本的 Windows。

http://wiki.delphi-jedi.org/wiki/JCL_Help:TJclMultiReadExclusiveWrite

http://wiki.delphi-jedi.org/index.php?title=JEDI_Code_Library

【讨论】:

【参考方案5】:

试试这个?可以作为普通变量使用:

type myclass=class
              Lock:TOBRWLock;
              function ReadIt:Integer;
              procedure WriteIt(A:Integer);
             end;  
function ReadIt:Integer;
begin;
 Lock.LockRead;
 Result:=GetVal;
 Lock.UnLockRead;
end;

还有很大的改进空间,您可以从这里构建有利于读写的变体,或者只是根据需要采取不同的行动。

const ldFree    = 0;
      ldReading = 1;
      ldWriting = 2;
type TOBRWLock          = record
                 [Volatile]WritersWaiting,
                           ReadersWaiting,
                           ReadersReading,
                           Disposition    : Integer;
                           procedure LockRead;
                           procedure LockWrite;
                           procedure UnlockRead;
                           procedure UnlockWrite;
                           procedure UnReadWrite;
                           procedure UnWriteRead;
                          end;

procedure TOBRWLock.LockRead;
var SpinCnt : NativeUInt;
    I       : Integer;
begin
 SpinCnt:=0;
 TInterlocked.Increment(ReadersWaiting);
 repeat
  if (Disposition=ldReading)
     then begin
           I:=TInterlocked.Increment(ReadersReading);
           if (Disposition<>ldReading) or (I=1)(*Only 1 read reference or Disposition changed, suspicious, rather retry*)
              then begin
                    TInterlocked.Decrement(ReadersReading);
                    continue;
                   end
              else begin(*Success*)
                    TInterlocked.Decrement(ReadersWaiting);
                    break;
                   end;
          end;
  if (WritersWaiting<>0)or(Disposition<>ldFree)
     then begin
           SpinBackoff(SpinCnt);
           continue;
          end;
  if TInterlocked.CompareExchange(Disposition,ldReading,ldFree)=ldFree
     then begin
           TInterlocked.Increment(ReadersReading);
           TInterlocked.Decrement(ReadersWaiting);
           break;
          end;
  SpinBackoff(SpinCnt);
 until False;
end;

procedure TOBRWLock.LockWrite;
var SpinCnt : NativeUInt;
begin
 SpinCnt:=0;
 TInterlocked.Increment(WritersWaiting);
 repeat
  if (Disposition<>ldFree)
     then begin
           SpinBackoff(SpinCnt);
           continue;
          end;
  if TInterlocked.CompareExchange(Disposition,ldWriting,ldFree)=ldFree
     then begin
           TInterlocked.Decrement(WritersWaiting);
           break;
          end
     else SpinBackoff(SpinCnt);
 until False;
end;

procedure TOBRWLock.UnlockRead;
begin
 $IFDEF DEBUG
 if Disposition<>ldReading
    then raise Exception.Create('UnlockRead a lock that is not Reading');
 $ENDIF
 TInterlocked.Decrement(ReadersReading);
 if ReadersReading=0
    then begin;
          if TInterlocked.CompareExchange(Disposition,ldFree,ldReading)<>ldReading
             then raise Exception.Create('Impossible 310');
         end;
end;

procedure TOBRWLock.UnlockWrite;
begin
 $IFDEF DEBUG
 if Disposition<>ldWriting
    then raise Exception.Create('UnlockWrite a lock that is not Writing');
 $ENDIF
 if TInterlocked.CompareExchange(Disposition,ldFree,ldWriting)<>ldWriting
    then raise Exception.Create('Impossible 321');
end;

procedure TOBRWLock.UnReadWrite;
var SpinCnt : NativeUInt;
begin
 $IFDEF DEBUG
 if Disposition<>ldReading
    then raise Exception.Create('UnReadWrite a lock that is not Reading');
 $ENDIF
 TInterlocked.Increment(WritersWaiting);
 SpinCnt:=0;
 repeat
  if ReadersReading=1(*Only me reading*)
     then begin;
           if TInterlocked.CompareExchange(Disposition,ldWriting,ldReading)<>ldReading(*Must always succeed*)
              then raise Exception.Create('Impossible 337');
           TInterlocked.Decrement(ReadersReading);
           TInterlocked.Decrement(WritersWaiting);
           break;
          end;
  SpinBackoff(SpinCnt);
 until False;
end;

【讨论】:

以上是关于更快的 TMultiReadExclusiveWriteSynchronizer?的主要内容,如果未能解决你的问题,请参考以下文章

PHP 如何更快地执行这些检查/或更快地加载页面?

有啥比 dict() 更快的吗?

在 MYSQL 中哪个更快?

为啥这个循环比创建字典的字典理解更快?

为啥批量插入/更新更快?批量更新如何工作?

更快的 TMultiReadExclusiveWriteSynchronizer?