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

时间:2012-05-28 07:13:30

标签: c++ multithreading compiler-construction thread-safety volatile

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

  

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

  • 在哪种情况下,编译器会将值存储在寄存器中并推迟更新主存储器? [关于以上引用]
  • 上述引言所说的“重新订购”是什么?它会在什么情况下发生?

2 个答案:

答案 0 :(得分:2)

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

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

问:上述引言所说的“重新订购”是什么?

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

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

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

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

答案 1 :(得分:0)

嗯...在搜索“volatile”keyword..lol时发现了这个 1.即使使用缓存,寄存器访问也比内存快很多。例如,如果您有以下内容:

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

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

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..lol)。