对共享数据的线程安全访问 - 读/写实际发生,不会发生重新排序

Thread safe access to shared data - read/write actually happens and no reordering takes place

本文关键字:新排序 排序 线程 数据 共享 安全 访问      更新时间:2023-10-16

从这里: https://stackoverflow.com/a/2485177/462608

对于对共享数据的线程安全访问,我们需要保证
读/写实际发生(编译器不会只是将值存储在寄存器中,而是将更新主内存推迟到很久以后)
不会发生重新排序。假设我们使用一个易失变量作为标志来指示某些数据是否已准备好 被阅读。在我们的代码中,我们只是在准备 数据,所以一切看起来都很好。但是,如果指令被重新排序怎么办 所以旗帜是先设置的?

  • 在哪些情况下,编译器将值存储在寄存器中并延迟更新主内存? [关于上述引文]
  • 上面引用所说的"重新排序"是什么?在什么情况下会发生?

问:在哪些情况下,编译器会将值存储在寄存器中并延迟更新主内存?

答:(这是一个广泛而开放式的问题,可能不太适合stackoverflow格式。简短的回答是,只要源语言的语义(C++每个标签)允许它并且编译器认为它是有利可图的。

问:上面引用所说的"重新排序"是什么?

答:编译器和/或 CPU 以不同于原始程序源的一对一翻译所指示的顺序发出加载和存储指令。

问:在什么情况下会发生?

答:对于编译器来说,与第一个问题的答案类似,只要原始程序语义允许并且编译器认为它是有利可图的。对于 CPU 来说,情况类似,CPU 可以根据体系结构内存模型,对内存访问进行重新排序,只要原始(单线程!)结果相同。例如,编译器和 CPU 都可以尝试尽早提升负载,因为负载延迟通常对性能至关重要。

为了执行更严格的排序,例如实现同步原语,CPU提供了各种原子和/或围栏指令,编译器可能会根据编译器和源语言提供禁止重新排序的方法。

嗯...在搜索"易失性"关键字时发现了这一点。哈哈1.即使使用缓存,寄存器访问也比内存快得多。例如,如果您有如下所示的内容:

for(i = 0; i < 10000; i++)
{
// whatever...
}

如果变量 i 存储在寄存器中,则循环的性能要好得多。因此,某些编译器可能会生成将 i 存储在寄存器中的代码。在循环结束之前,内存中可能不会发生对该变量的更新。甚至完全有可能 i 永远不会被写入内存(例如,我以后永远不会使用)或溢出在循环体中(例如,内部有一个更重的嵌套循环需要优化,并且没有更多的寄存器)。这种技术称为寄存器分配。一般来说,只要语言标准允许,优化器就没有规则。它有很多不同的算法。当它发生时很难回答。这就是詹纳布这么说的原因。如果变量没有及时更新,对于多线程代码来说,它可能非常糟糕。例如,如果您有如下代码:

bool objRead = false;
createThread(prepareObj);  // objReady will be turn on in prepareObj thread.
while(!objReady) Sleep(100);
obj->doSomething();

优化器可能生成的代码只测试 objReady 一次(当控制流进入循环时),因为它不会在循环内更改。这就是为什么我们需要确保读写真正按照我们在多线程代码中设计的方式进行。

重新排序比寄存器分配更复杂。编译器和 CPU 都可能会更改代码的执行顺序。

void prepareObj()
{
obj = &something;
objReady = true;
}

从 prepareObj 函数的角度来看,我们先设置 objReady 还是先设置 obj 指针并不重要。编译器和 CPU 可能会出于不同的原因颠倒两个指令的顺序,例如特定 CPU 管道上更好的并行性、缓存命中更好的数据局部性。你可以阅读janneb推荐的《计算机体系结构:定量方法》一书。如果我没记错的话,附录A是关于重新排序的(如果没有,请转到附录B或C。哈哈)。