我有一个内存数据结构,由多个线程读取并且只由一个线程写入。目前我正在使用一个关键部分来使这个访问线程安全。不幸的是,即使只有另一个读者访问它,这也会阻止读者。
有两种方法可以解决这个问题:
对于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的一个特例。
答案 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,只留下低位。
要获得读者数量,只需将访问模式右移一个。