使用关键部分避免Delphi中的缓存一致性问题?

Tro*_*roy 11 delphi multithreading multiprocessing critical-section

我刚刚阅读了MSDN文章"同步和多处理器问题",该文章解决了多处理器计算机上的内存缓存一致性问题.这真的让我大开眼界,因为我不会想到他们提供的例子中会有竞争条件.本文解释了对内存的写入可能实际上并不是按照我的代码中编写的顺序发生的(从其他cpu的角度来看).这对我来说是个新概念!

本文提供了2种解决方案:

  1. 在需要跨多个cpus的缓存一致性的变量上使用"volatile"关键字.这是一个C/C++关键字,在Delphi中不可用.
  2. 使用InterlockExchange()和InterlockCompareExchange().如果必须的话,我可以在德尔福做这件事.它看起来有点乱.

文章还提到"以下同步函数使用适当的障碍来确保内存排序:•进入或离开关键部分的函数".

这是我不理解的部分.这是否意味着对限制使用关键部分的函数的内存写入不受缓存一致性和内存排序问题的影响?我对Interlock*()函数没有任何反对意见,但我的工具带中的另一个工具会很棒!

Arn*_*hez 8

这篇MSDN文章只是多线程应用程序开发的第一步:简而言之,它意味着"使用锁(也称为关键部分)保护您的共享变量",因为您不确定您读/写的数据是否对所有人都相同线程".

CPU每核心缓存只是可能的问题之一,这将导致读取错误的值.可能导致竞争条件的另一个问题是两个线程同时写入资源:之后不可能知道将存储哪个值.

由于代码期望数据一致,因此某些多线程程序可能表现错误.对于多线程,当您处理共享变量时,您不确定通过单个指令编写的代码是否按预期执行.

InterlockedExchange/InterlockedIncrement函数是具有LOCK前缀的低级asm操作码(或者由设计锁定,如XCHG EDX,[EAX]操作码),这确实会强制所有CPU内核的高速缓存一致性,因此使asm操作码执行线程安全.

例如,下面是在分配字符串值时如何实现字符串引用计数(请参阅_LStrAsgSystem.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]   
            ...
Run Code Online (Sandbox Code Playgroud)

第一个INC ECXLOCK 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;
Run Code Online (Sandbox Code Playgroud)

使用局部变量可使关键部分更短,因此您的应用程序将更好地扩展并充分利用CPU内核的全部功能.在EnterCriticalSection和之间LeaveCriticalSection,只有一个线程在运行:其他线程将在EnterCriticalSection调用中等待...因此,临界区越短,应用程序就越快.一些错误设计的多线程应用程序实际上可能比单线程应用程序慢!

并且不要忘记,如果关键部分中的代码可能引发异常,则应始终编写显式try ... finally LeaveCriticalSection() end;块以保护锁定释放,并防止应用程序的任何死锁.

如果用锁(即关键部分)保护共享数据,Delphi是完全线程安全的.请注意,即使在其RTL函数中存在LOCK,也应该保护引用计数变量(如字符串):此LOCK用于假设正确的引用计数并避免内存泄漏,但它不是线程安全的.为了使其尽可能快,请参阅此SO问题.

的目的InterlockExchangeInterlockCompareExchange是改变共享指针变量值.您可以将其视为访问指针值的关键部分的"轻型"版本.

在所有情况下,编写工作多线程代码并不容易 - 它甚至很难,正如Delphi专家刚刚在他的博客中写的那样.

您应该编写完全没有共享数据的简单线程(在线程启动之前创建数据的私有副本,或者使用只读共享数据 - 本质上是线程安全的),或者调用一些设计良好且经过验证的库- 像http://otl.17slon.com - 这将节省大量的调试时间.


Dav*_*nan 7

首先,根据语言标准,volatile不会像文章所说的那样做.volatile的获取和释放语义是MSVC特定的.如果您使用其他编译器或其他平台进行编译,则可能会出现问题.C++ 11引入了语言支持的原子变量,希望在适当的时候最终结束使用volatile作为线程构造的(错误)使用.

确实实现了关键部分和互斥锁,以便从所有线程中正确地看到受保护变量的读写.

我认为考虑关键部分和互斥锁(锁)的最佳方式是实现序列化的设备.也就是说,由这种锁保护的代码块是一个接一个地连续执行而没有重叠.序列化也适用于内存访问.由于高速缓存一致性或读/写重新排序,不存在任何问题.

使用存储器总线上的基于硬件的锁实现互锁功能.这些函数由无锁算法使用.这意味着他们不使用像重要部分那样的重型锁,而是使用这些重量轻的硬件锁.

无锁算法比基于锁的算法更有效,但无锁算法可能非常难以正确编写.除非性能影响是可辨别的,否则优先选择关键部分而不是锁定.

另一篇值得一读的文章是"双重锁定破损"声明.