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

问题描述 投票:0回答:2

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

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

  • 在什么情况下编译器会将值存储在寄存器中并推迟更新主内存? [关于上述引述]
  • 上面引用的“重新排序”是什么?什么情况下会发生?
c++ multithreading thread-safety volatile
2个回答
2
投票

问: 在什么情况下编译器会将值存储在寄存器中并推迟更新主内存?

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

问: 上面引用的“重新排序”是什么?

A: 编译器和/或 CPU 发出加载和存储指令的顺序与原始程序源的一对一翻译所规定的顺序不同。

问: 什么情况下会发生?

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

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


0
投票

嗯...在搜索“挥发性”关键字时发现了这个..lol 1. 即使有缓存,寄存器访问也比内存快得多。例如,如果您有如下内容:

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

如果变量 i 存储在寄存器中,则循环的性能会更好。因此,某些编译器可能会生成将 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推荐的《Computer Architecture: A Quantitative Approach》一书。如果我没记错的话,附录 A 是关于重新排序的(如果没有,请转到附录 B 或 C..lol)。

© www.soinside.com 2019 - 2024. All rights reserved.