c++ - 如果我们使用内存防护来增强一致性,那么 “thread-thrashing”会如何发生?

标签 c++ multithreading concurrency x86 cpu

在我知道CPU的存储缓冲区之前,我曾以为只有两个线程想要写入同一缓存行时,才会发生线程冲突。一个会阻止另一个人写。但是,这似乎很同步。后来,我了解到有一个存储缓冲区,可以临时刷新写入内容。它被强制通过SFENCE指令进行刷新,这意味着没有同步阻止多个内核访问同一高速缓存行...。

如果我们必须谨慎并使用SFENCE,我完全会困惑如何发生线程颠簸?线程颠簸意味着阻塞,而SFENCEs意味着写操作是异步完成的,程序员必须手动刷新写操作?

(我对SFENCE的理解可能也很困惑-因为我还读到Intel内存模型是“强壮的”,因此只有字符串x86指令才需要内存围栏)。

有人可以消除我的困惑吗?

“抖动”是指多个内核检索相同的cpu缓存行,这会导致争用同一缓存行的其他内核的延迟开销。

最佳答案

因此,至少在我的词汇中,当您遇到以下情况时,就会发生线程颠簸:

  // global variable
  int x;

  // Thread 1
  void thread1_code()
  {
    while(!done)
      x++;
  }

  // Thread 2
  void thread2_code()
  {
    while(!done)
      x++;
  }

(这段代码当然完全是胡说八道-我在说它很简单,但毫无意义,因为没有复杂的代码来解释线程本身正在发生的事情是很复杂的)

为简单起见,我们假设线程1始终在处理器1上运行,线程2始终在处理器2上运行[1]

如果您在SMP系统上运行这两个线程-并且我们刚刚启动了此代码[两个线程几乎完全同时启动,而实际上在魔术中,这与实际系统中不同,相隔数千个时钟周期],线程一个将读取x的值,对其进行更新,然后将其写回。到目前为止,线程2也正在运行,并且还将读取x的值,对其进行更新并将其写回。为此,它实际上需要询问其他处理器“您的缓存中是否有x(具有新值),如果是,请给我一份拷贝”。当然,处理器1将有一个新值,因为它刚刚存储了x的值。现在,该缓存行是“共享的”(我们的两个线程都具有该值的拷贝)。线程二更新该值并将其写回内存。这样做时,此处理器将发送另一个信号,表明“如果有人持有x的值,请删除它,因为我刚刚更新了该值”。

当然,两个线程完全有可能读取相同的x值,更新为相同的新值,然后将其写回为相同的新修改后的值。一个处理器迟早会回写一个比另一个处理器写的值低的值,因为它落后了一点……

篱笆操作将有助于确保写入内存的数据在下一次操作发生之前实际上已全部缓存,因为正如您所说,有写缓冲区可以在实际到达内存之前保存内存更新。如果您没有栅栏指令,则您的处理器可能会严重地异相,并在另一个没有时间说“您对x是否有新值吗?”之前多次更新该值。 -但是,这并不能真正防止阻止处理器1向处理器2询问数据,并且处理器2立即向其“返回”询问,从而尽可能快地来回ping高速缓存内容。

为了确保只有一个处理器更新某些共享值,需要使用所谓的原子指令。这些特殊的指令旨在与写缓冲区和高速缓存一起使用,从而确保只有一个处理器实际上持有要更新的高速缓存行的最新值,而其他任何处理器都无法更新该处理器完成更新之前的值。因此,您永远不会“读取相同的x值并写回相同的x值”或任何类似的东西。

由于高速缓存不适用于单个字节或单个整数大小的事物,因此您也可以使用“错误共享”。例如:
 int x, y;

 void thread1_code()
 {
    while(!done) x++;
 }

 void thread2_code()
 {
    while(!done) y++;
 }

现在,xy实际上不是相同的变量,但是它们(看起来合理,但我们不能百分百确定)位于16、32、64或128字节的同一缓存行中(取决于处理器)建筑学)。因此,尽管xy是不同的,但是当一个处理器说“我刚刚更新了x,请删除所有拷贝”时,另一处理器将在摆脱的同时摆脱它的y值(仍是正确的)的x。我有一个这样的例子,其中一些代码正在执行:
 struct {
    int x[num_threads];
    ... lots more stuff in the same way
 } global_var;

 void thread_code()
 {
    ...
     global_var.x[my_thread_number]++;
    ...
 }

当然,然后两个线程将立即彼此更新值,并且性能为RUBBISH(比通过以下方法固定时慢大约6倍:
struct
{
   int x;
   ... more stuff here ... 
} global_var[num_threads]; 

 void thread_code()
 {
    ...
     global_var[my_thread_number].x++;
    ...
 }

编辑以澄清:fence不能(如我最近的编辑所解释的那样)“帮助”阻止对线程之间的缓存内容进行ping操作。它本身并不能阻止数据在处理器之间的不同步更新-但是,它可以确保执行fence操作的处理器在该特定操作的内存内容之前不会继续进行其他内存操作已经“脱离”了处理器核心本身。由于存在各种流水线阶段,并且大多数现代CPU具有多个执行单元,因此一个单元很可能在执行流中技术上“落后”的另一个单元的“前面”。围栏将确保“此处已完成所有工作”。这有点像是一级方程式赛车中具有大限位盘的人,它可以确保驾驶员在所有新轮胎都牢牢固定在车上之前(如果每个人都按其应做的那样)不离开轮胎更换。

MESI或MOESI协议(protocol)是一种状态机系统,可确保正确处理不同处理器之间的操作。处理器可以具有修改后的值(在这种情况下,信号将发送到所有其他处理器以“停止使用旧值”),处理器可以“拥有”该值(它是此数据的所有者,并且可以修改该值)。值),处理器可能具有“排他”值(它是该值的唯一持有者,其他所有人都摆脱了其拷贝),它可能是“共享的”(一个处理器拥有一个以上的拷贝,但是该处理器不应更新值-它不是数据的“所有者”或无效(数据不在缓存中)。 MESI不具有“拥有”模式,这意味着监听总线上的流量更多(“snoop”表示“您是否有x的拷贝”,“请删除您的x的拷贝”等)

[1]是的,处理器编号通常以零开始,但是当我撰写此附加段落时,我不介意回去将thread1重命名为thread0,将thread2重命名为thread1。

关于c++ - 如果我们使用内存防护来增强一致性,那么 “thread-thrashing”会如何发生?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28753116/

相关文章:

c++ - 默认模板参数的范围是什么?

c++ - 可变线程与非常量方法

C#相当于Objective-C的dispatch_group和queue?

android - onperformsync 和多线程

java - 有没有更简单的方法来使用并发映射锁?

java - 使用线程(启动和停止它们)

c++ - 如何将std::get用作boost-multi-index容器键的global_fun?

C++类设计题

c++ - 使用 Intel archiver 命令而不是 ar 的 boost-build(又名 bjam)

c - 使用 CAS 以原子方式递增两个整数