锁定多个读者单一作家

时间:2009-05-26 07:48:57

标签: multithreading delphi lock-free

我有一个内存数据结构,由多个线程读取并且只由一个线程写入。目前我正在使用一个关键部分来使这个访问线程安全。不幸的是,即使只有另一个读者访问它,这也会阻止读者。

有两种方法可以解决这个问题:

  1. 使用TMultiReadExclusiveWriteSynchronizer
  2. 使用无锁方法取消任何阻止
  3. 对于2.到目前为止我已经得到了以下内容(任何无关紧要的代码都被遗漏了):

    type
      TDataManager = class
      private
        FAccessCount: integer;
        FData: TDataClass;
      public
        procedure Read(out _Some: integer; out _Data: double);
        procedure Write(_Some: integer; _Data: double);
      end;
    
    procedure TDataManager.Read(out _Some: integer; out _Data: double);
    var
      Data: TDAtaClass;
    begin
      InterlockedIncrement(FAccessCount);
      try
        // make sure we get both values from the same TDataClass instance
        Data := FData;
        // read the actual data
        _Some := Data.Some;
        _Data := Data.Data;
      finally
        InterlockedDecrement(FAccessCount);
      end;
    end;
    
    procedure TDataManager.Write(_Some: integer; _Data: double);
    var
      NewData: TDataClass;
      OldData: TDataClass;
      ReaderCount: integer;
    begin
      NewData := TDataClass.Create(_Some, _Data);
      InterlockedIncrement(FAccessCount);
      OldData := TDataClass(InterlockedExchange(integer(FData), integer(NewData));
      // now FData points to the new instance but there might still be
      // readers that got the old one before we exchanged it.
      ReaderCount := InterlockedDecrement(FAccessCount);
      if ReaderCount = 0 then
        // no active readers, so we can safely free the old instance
        FreeAndNil(OldData)
      else begin
        /// here is the problem
      end;
    end;
    

    不幸的是,在替换之后摆脱OldData实例的问题很小。如果Read方法中当前没有其他线程(ReaderCount = 0),则可以安全地处理它,就是这样。但如果情况并非如此,我该怎么办? 我可以将它存储到下一个调用并将其置于那里,但Windows调度理论上可以让读者线程在Read方法内进行休眠,并且仍然有对OldData的引用。

    如果您发现上述代码存在任何其他问题,请告诉我相关信息。这将在具有多个核心的计算机上运行,​​并且上述方法将被非常频繁地调用。

    如果这很重要:我正在使用Delphi 2007和内置内存管理器。我知道内存管理器在创建新类时可能会强制执行某些锁定,但我暂时想忽略它。

    编辑:上面可能还不清楚:对于TDataManager对象的完整生命周期,只有一个线程写入数据,而不是几个可能竞争写访问的线程。所以这是MREW的一个特例。

4 个答案:

答案 0 :(得分:6)

我不知道任何无锁(或上面示例中的微锁定)MREW方法可以在Intel86代码上实现。

对于小型(快速到期)锁定,来自OmniThreadLibrary的旋转方法可以正常工作:

type
TOmniMREW = record
strict private
  omrewReference: integer;      //Reference.Bit0 is 'writing in progress' flag
public
  procedure EnterReadLock; inline;
  procedure EnterWriteLock; inline;
  procedure ExitReadLock; inline;
  procedure ExitWriteLock; inline;
end; { TOmniMREW }

procedure TOmniMREW.EnterReadLock;
var
  currentReference: integer;
begin
  //Wait on writer to reset write flag so Reference.Bit0 must be 0 than increase Reference
  repeat
    currentReference := omrewReference AND NOT 1;
  until currentReference = InterlockedCompareExchange(omrewReference, currentReference + 2, currentReference);
end; { TOmniMREW.EnterReadLock }

procedure TOmniMREW.EnterWriteLock;
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 = InterlockedCompareExchange(omrewReference, currentReference + 1, currentReference);
  //Now wait on all readers
  repeat
  until omrewReference = 1;
end; { TOmniMREW.EnterWriteLock }

procedure TOmniMREW.ExitReadLock;
begin
  //Decrease omrewReference
  InterlockedExchangeAdd(omrewReference, -2);
end; { TOmniMREW.ExitReadLock }

procedure TOmniMREW.ExitWriteLock;
begin
  omrewReference := 0;
end; { TOmniMREW.ExitWriteLock }

我刚刚发现了一个可能的对齐问题 - 代码应该检查omrewReference是否为4对齐。将通知作者。

答案 1 :(得分:0)

只是一个补充 - 你在这里看到的通常被称为Hazard Pointers。我不知道你是否可以在Delphi中做类似的事情。

答案 2 :(得分:0)

自从我在Delphi中弄脏之后已经有一段时间了,所以在使用前验证这一点,但是......如果使用接口和使用TInterfacedObject的实现,则可以从内存中获取引用计数行为。

type
    IDataClass = interface
        function GetSome: integer;
        function GetData: double;

        property Some: integer read GetSome;
        property Data: double read GetData;
    end;

    TDataClass = class(TInterfacedObject, IDataClass)
    private
        FSome: integer;
        FData: double;
    protected
        function GetSome: integer;
        function GetData: double;
    public
        constructor Create(ASome: integer; AData: double);
    end;

然后你改为ISomeData类型的所有变量(混合ISomeData和TSomeData是一个非常糟糕的主意......你很容易得到引用计数问题)。

基本上这会导致引用计数在读取器代码中自动递增,它会加载对数据的本地引用,并在变量离开作用域时递减,此时它将在那里取消分配。

我知道在接口和类实现中复制数据类的API有点乏味,但这是获得所需行为的最简单方法。

答案 3 :(得分:0)

我有一个潜在的解决方案给你;它可以让新读者随时开始,直到作者希望写作。然后,作者等待读者完成并执行其写入。写完之后,读者可以再次阅读。

此外,此解决方案不需要锁或互斥锁,但它确实需要原子测试和设置操作。我不知道Delphi和我在Lisp中编写了我的解决方案,因此我将尝试用伪代码来描述它。

(CAPS是函数名称,所有这些函数都接受并且不返回任何参数)

integer access-mode = 1; // start in reader mode. 

WRITE  loop with current = accessmode, 
            with new = (current & 0xFFFFFFFe) 
            until test-and-set(access-mode, current to new)
       loop until access-mode = 0; 

ENDWRITE assert( access-mode = 0)
         set access-mode to 1

READ loop with current = ( accessmode | 1 ),
          with new = (current + 2),
          until test-and-set(access-mode, current to new)
ENDREAD loop with current = accessmode
             with new = (current - 2),
             until test-and-set(access-mode, current to new)

要使用,读者在阅读前调用READ,完成后调用ENDREAD。单独的作者在写作之前调用WRITE,在完成之后调用ENDWRITE。

这个想法是一个称为访问模式的整数,它在最低位保存一个布尔值,并在其中包含一个计数 更高的位。 WRITE将该位设置为0,然后旋转,直到足够的ENDREAD计数到访问模式为零。 Endwrite将访问模式设置为1.将当前访问模式读取为1,因此只有低位开始时,它们的测试和设置才会通过。我加2并减去2,只留下低位。

要获得读者数量,只需将访问模式右移一个。