锁定多个读者单个作家
我有一个内存数据结构,可由多个线程读取并仅由一个线程写入。 目前我正在使用关键部分来使此访问线程安全。 不幸的是,即使只有另一个读者正在访问它,这也会阻止读者。
有两个选项可以解决这个问题:
- 使用 TMultiReadExclusiveWriteSynchronizer
- 通过使用无锁方法消除任何阻塞
For 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 的一个特例。
I have got an in memory data structure that is read by multiple threads and written by only one thread. Currently I am using a critical section to make this access threadsafe. Unfortunately this has the effect of blocking readers even though only another reader is accessing it.
There are two options to remedy this:
- use TMultiReadExclusiveWriteSynchronizer
- do away with any blocking by using a lock free approach
For 2. I have got the following so far (any code that doesn't matter has been left out):
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;
Unfortunately there is the small problem of getting rid of the OldData instance after it has been replaced. If no other thread is currently within the Read method (ReaderCount=0), it can safely be disposed and that's it. But what can I do if that's not the case?
I could just store it until the next call and dispose it there, but Windows scheduling could in theory let a reader thread sleep while it is within the Read method and still has got a reference to OldData.
If you see any other problem with the above code, please tell me about it. This is to be run on computers with multiple cores and the above methods are to be called very frequently.
In case this matters: I am using Delphi 2007 with the builtin memory manager. I am aware that the memory manager probably enforces some lock anyway when creating a new class but I want to ignore that for the moment.
Edit: It may not have been clear from the above: For the full lifetime of the TDataManager object there is only one thread that writes to the data, not several that might compete for write access. So this is a special case of MREW.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
我不知道有任何无锁(或上面示例中的微锁定)MREW 方法可以在 Intel86 代码上实现。
对于小型(快速过期)锁,可以使用 OmniThreadLibrary 工作正常:
我刚刚注意到这里可能存在对齐问题 - 代码应该检查 omrewReference 是否是 4 对齐的。 会通知作者。
I don't know of any lock-free (or micro-locking as in your example above) MREW approach that could be implemented on Intel86 code.
For small (fast-expiring) locks a spinning approach from the OmniThreadLibrary works fine:
I just noticed a possible alignment issue here - the code should check that omrewReference is 4-aligned. Will notify the author.
只是补充 - 您在这里看到的通常称为 危险提示。 我不知道你是否可以在 Delphi 中做类似的事情。
Just an addition - what you are looking at here is generally known as Hazard Pointers. I have no idea if you can do something similar in Delphi.
自从我接触 Delphi 以来已经有一段时间了,所以在使用之前验证一下这一点,但是......根据记忆,如果您使用接口和使用 TInterfacedObject 的实现,您可以获得引用计数行为。
然后你将所有变量设置为 ISomeData 类型(混合 ISomeData 和 TSomeData 是一个非常糟糕的主意......你很容易遇到引用计数问题)。
基本上,这会导致引用计数在读取器代码中自动递增,其中它加载对数据的本地引用,并且当变量离开作用域时引用计数会递减,此时它将在那里取消分配。
我知道在接口和类实现中复制数据类的 API 有点乏味,但这是获得所需行为的最简单方法。
It's been a while since I got my hands dirty in Delphi, so verify this before using, but... from memory, you can get reference-counted behaviour if you use an interface and an implementation using TInterfacedObject.
Then you make all your variables of type ISomeData instead (mixing ISomeData and TSomeData is a very bad idea... you easily get reference-counting problems).
Basically this would cause the reference count to increment automatically in your reader code where it loads the local reference to the data, and it gets decremented when the variable leaves scope, at which point it would de-allocate there.
I know it's a bit tedious to duplicate the API of your data class in an interface and a class implementation, but it is the easiest way to get your desired behaviour.
我为你提供了一个可能的解决方案; 它可以让新读者随时开始,直到作者愿意写作为止。 然后,作者等待读者完成并执行其写入。 写完后,读者可以再次阅读。
此外,该解决方案不需要锁或互斥体,但它确实需要原子测试和设置操作。 我不了解 Delphi,我用 Lisp 编写了我的解决方案,所以我将尝试用伪代码来描述它。
(大写字母是函数名,所有这些函数都不接受和返回任何参数)
使用时,读取器在读取之前调用 READ,在完成时调用 ENDREAD。 单独的写入者在写入之前调用 WRITE,并在完成后调用 ENDWRITE。
这个想法是一个称为访问模式的整数在最低位保存一个布尔值,并在
较高位。 WRITE 将该位设置为 0,然后旋转,直到足够的 ENDREAD 数向下计数访问模式为零。 Endwrite 将访问模式设置回 1。将当前访问模式与 1 进行 READ OR 运算,因此仅当低位一开始就为高时,它们的测试和设置才会通过。 我加减 2 以保留低位。
要获得读者计数,只需将访问模式右移一位即可。
I've got a potential solution for you; it lets new readers start anytime until the writer wishes to write. The writer then waits for the readers to finish and performs its write. After the writing is done the readers can read once more.
Furthermore, this solution does not need locks or mutexs, but it does need an atomic test-and-set operation. I don't know Delphi and I wrote my solution in Lisp, so I'll try to describe it in pseudo code.
(CAPS are function names, all these functions take and return no arguments)
To use, a reader calls READ before reading and ENDREAD when done. The lone writer calls WRITE before writing and ENDWRITE when done.
The idea is an integer called access-mode holds a boolean in the lowest bit, and a count in
the higher bits. WRITE sets the bit to 0, and then spins until the enough ENDREADs count down access mode to zero. Endwrite sets access-mode back to 1. READ ORs the current access-mode with 1, so their test-and-set will only ever pass if the low-bit was high to begin with. I add and subtract by 2 to leave the low-bit alone.
To get a count of readers just take access-mode right shifted by one.