这篇 MSDN 文章只是多线程应用程序开发的第一步:简而言之,它的意思是“用锁(也称为临界区)保护您的共享变量,因为您不确定您读/写的数据是否对所有人都相同线程”。
CPU 每核缓存只是可能的问题之一,这将导致读取错误的值。另一个可能导致竞争条件的问题是两个线程同时写入资源:不可能知道之后将存储哪个值。
由于代码期望数据是连贯的,因此某些多线程程序可能会出现错误的行为。使用多线程,当处理共享变量时,您不确定通过单独指令编写的代码是否按预期执行。
InterlockedExchange/InterlockedIncrement
函数是带有 LOCK 前缀的低级 asm 操作码(或通过设计锁定,如XCHG EDX,[EAX]
操作码),这确实会强制所有 CPU 内核的缓存一致性,因此使 asm 操作码执行线程安全。
例如,当您分配字符串值时,这是如何实现字符串引用计数的(参见_LStrAsg
System.pas - 这是来自我们为 Delphi 7/2002 优化的 RTL 版本- 因为 Delphi 原始代码受版权保护):
MOV ECX,[EDX-skew].StrRec.refCnt
INC ECX { thread-unsafe increment ECX = reference count }
JG @@1 { ECX=-1 -> literal string -> jump not taken }
.....
@@1: LOCK INC [EDX-skew].StrRec.refCnt { ATOMIC increment of reference count }
MOV ECX,[EAX]
...
第一个INC ECX
和LOCK INC [EDX-skew].StrRec.refCnt
- 不仅第一个增加 ECX 而不是引用计数变量,而且第一个不是线程安全的,而第二个以 LOCK 为前缀,因此将是线程安全的。
顺便说一句,这个 LOCK 前缀是RTL 中多线程扩展的问题之一- 使用较新的 CPU 更好,但仍然不完美。
因此,使用临界区是使代码线程安全的最简单方法:
var GlobalVariable: string;
GlobalSection: TRTLCriticalSection;
procedure TThreadOne.Execute;
var LocalVariable: string;
begin
...
EnterCriticalSection(GlobalSection);
LocalVariable := GlobalVariable+'a'; { modify GlobalVariable }
GlobalVariable := LocalVariable;
LeaveCriticalSection(GlobalSection);
....
end;
procedure TThreadTwp.Execute;
var LocalVariable: string;
begin
...
EnterCriticalSection(GlobalSection);
LocalVariable := GlobalVariable; { thread-safe read GlobalVariable }
LeaveCriticalSection(GlobalSection);
....
end;
使用局部变量可以缩短关键部分,因此您的应用程序将更好地扩展并充分利用 CPU 内核的全部功能。在EnterCriticalSection
和之间LeaveCriticalSection
,只有一个线程在运行:其他线程将等待EnterCriticalSection
调用......所以临界区越短,你的应用程序就越快。一些设计错误的多线程应用程序实际上可能比单线程应用程序慢!
并且不要忘记,如果您在临界区中的代码可能引发异常,您应该始终编写一个显式try ... finally LeaveCriticalSection() end;
块来保护锁释放,并防止您的应用程序出现任何死锁。
如果您使用锁(即关键部分)保护共享数据,Delphi 是完全线程安全的。请注意,即使引用计数的变量(如字符串)也应该受到保护,即使它们的 RTL 函数中有一个 LOCK:这个 LOCK 是用来假设正确的引用计数并避免内存泄漏,但它不是线程安全的. 为了使其尽可能快,请参阅这个 SO question。
InterlockExchange
and的目的InterlockCompareExchange
是改变一个共享指针变量的值。您可以将其视为访问指针值的关键部分的“轻量级”版本。
在所有情况下,编写可工作的多线程代码都不容易——甚至更难,正如 Delphi 专家刚刚在他的博客中所写的那样。
您应该编写根本没有共享数据的简单线程(在线程启动之前制作数据的私有副本,或者使用只读共享数据 - 本质上是线程安全的),或者调用一些设计良好且经过验证的库- 像http://otl.17slon.com - 这将为您节省大量调试时间。