C++ 如果我们使用内存围栏来加强一致性,那么;“线程抖动”;曾经发生过吗?

C++ 如果我们使用内存围栏来加强一致性,那么;“线程抖动”;曾经发生过吗?,c++,multithreading,concurrency,x86,cpu,C++,Multithreading,Concurrency,X86,Cpu,在我知道CPU的存储缓冲区之前,我认为当两个线程想要写入同一个缓存线时,就会发生线程抖动。一个会阻止另一个写作。然而,这似乎相当同步。我后来了解到,有一个存储缓冲区,可以临时刷新写入。它被迫刷新SFENCE指令,这意味着没有同步防止多个内核访问同一缓存线 我完全搞不懂线程抖动是如何发生的,如果我们必须小心使用SFENCEs的话?线程抖动意味着阻塞,而SFENCEs意味着写操作是异步完成的,程序员必须手动刷新写操作 (我对SFENCEs的理解可能也有点混淆——因为我还了解到Intel内存模型是“强

在我知道CPU的存储缓冲区之前,我认为当两个线程想要写入同一个缓存线时,就会发生线程抖动。一个会阻止另一个写作。然而,这似乎相当同步。我后来了解到,有一个存储缓冲区,可以临时刷新写入。它被迫刷新SFENCE指令,这意味着没有同步防止多个内核访问同一缓存线

我完全搞不懂线程抖动是如何发生的,如果我们必须小心使用SFENCEs的话?线程抖动意味着阻塞,而SFENCEs意味着写操作是异步完成的,程序员必须手动刷新写操作

(我对SFENCEs的理解可能也有点混淆——因为我还了解到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系统上运行这两个线程-我们刚刚启动了这段代码[两个线程几乎完全在同一时间启动,不像在实际系统中,相隔数千个时钟周期],线程1将读取
x
的值,对其进行更新,然后将其写回。现在,线程2也在运行,它还将读取
x
的值,更新它,然后写回。为此,它需要实际询问其他处理器“您的缓存中是否有(新值)
x
,如果有,请给我一份副本”。当然,处理器1将有一个新值,因为它刚刚存储了
x
的值。现在,缓存线是“共享的”(我们的两个线程都有一个值的副本)。线程2更新该值并将其写回内存。当它这样做时,该处理器会发送另一个信号,表示“如果有人持有
x
,请扔掉它,因为我刚刚更新了该值”

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

围栏操作将有助于确保写入内存的数据在下一个操作发生之前实际上已经完全进入缓存,因为正如您所说,在内存更新实际到达内存之前,有写缓冲区来保存它们。如果您没有围栏指令,您的处理器可能会严重不同步,并且在另一个处理器有时间说“您是否有新的
x
”之前多次更新该值-但是,这并不能真正帮助阻止处理器1从处理器2请求数据,而处理器2立即请求数据“back”,从而尽可能快地来回乒乓缓存内容

为了确保只有一个处理器更新某些共享值,您需要使用所谓的原子指令。这些特殊指令设计为与写缓冲区和缓存一起运行,以确保只有一个处理器实际保存正在更新的缓存线的最新值,并且没有在该处理器完成更新之前,其他处理器可以更新该值。因此,您永远不会得到“读取
x
的相同值并写回
x
的相同值”或任何类似的结果

由于缓存不能处理单个字节或单个整数大小的内容,因此也可以进行“错误共享”。例如:

 int x, y;

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

 void thread2_code()
 {
    while(!done) y++;
 }
现在,
x
y
实际上不是同一个变量,但它们(相当合理,但我们不能100%确定)位于16、32、64或128字节的同一缓存线内(取决于处理器体系结构)。因此,尽管
x
y
是不同的,当一个处理器说“我刚刚更新了
x
,请删除所有副本”,其他处理器将在删除
x
的同时删除它的
y
值(仍然正确)。我有这样一个示例,其中一些代码正在执行:

 struct {
    int x[num_threads];
    ... lots more stuff in the same way
 } global_var;

 void thread_code()
 {
    ...
     global_var.x[my_thread_number]++;
    ...
 }
当然,两个线程会紧靠在一起更新值,性能是垃圾(比我们通过执行以下操作修复时慢6倍左右):

struct
{
   int x;
   ... more stuff here ... 
} global_var[num_threads]; 

 void thread_code()
 {
    ...
     global_var[my_thread_number].x++;
    ...
 }
编辑以澄清:
fence
没有(正如我最近的编辑所解释的)“帮助”"防止在线程之间对缓存内容进行乒乓。它本身也不会防止处理器之间的数据更新不同步-但是,它会确保执行
fence
操作的处理器不会继续执行其他内存操作,直到此特定操作内存内容消失处理器内核本身“超出”范围。由于存在不同的流水线级,并且大多数现代CPU都有多个执行单元,因此在执行流中,一个单元可能在技术上“落后”的另一个单元之前“领先”。栅栏将确保“一切都在这里完成”.这有点像一级方程式赛车中带大挡块的人,确保车手在所有新轮胎安全安装在车上之前不会因更换轮胎而离开(如果每个人都这样做的话