在我审核的代码库中,我发现了以下习语。
void notify(struct actor_t act) {
write(act.pipe, "M", 1);
}
// thread A sending data to thread B
void send(byte *data) {
global.data = data;
notify(threadB);
}
// in thread B event loop
read(this.sock, &cmd, 1);
switch (cmd) {
case 'M': use_data(global.data);break;
...
}
“抓住它”,我对作者说,我的团队的一名高级成员,“这里没有内存障碍!你不能保证将global.data
从缓存刷新到主内存。如果线程A和线程B将在两个不同的处理器中运行 - 这种方案可能会失败“。
“但是,它在书中说......”
“安静!”,他立刻安慰我,“也许理论上,它不能得到保证,但在实践中,你使用函数调用的事实实际上是一个内存障碍。编译器不会重新排序指令global.data = data
,因为它无法知道是否有人在函数调用中使用它,并且x86架构将确保其他CPU在线程B从管道读取命令时将看到这条全局数据。请放心,我们有需要担心充足的现实世界问题。我们不需要在虚假的理论问题上投入额外的精力。
“请放心,我的孩子,你会理解将真正的问题与我需要获得博士的非问题分开。”
他是对的吗?这在实践中真的不是问题(例如x86,x64和ARM)吗?
这是我所学到的一切,但他确实有一个长长的胡须和一个非常聪明的外表!
如果你能告诉我一段证明他错误的代码,那就加分吧!
答案 0 :(得分:10)
内存障碍不仅仅是为了防止指令重新排序。即使指令没有重新排序,它仍然可能导致缓存一致性问题。至于重新排序 - 它取决于您的编译器和设置。 ICC对重新排序特别激进。 MSVC w /整个程序优化也可以。
如果您的共享数据变量声明为volatile
,即使它不在规范中,大多数编译器都会生成一个围绕变量读写的内存变量,并阻止重新排序。 这不是使用volatile
的正确方法,也不是它的用途。
(如果我还有任何选票,我会为你的叙述+1你的问题。)
答案 1 :(得分:8)
实际上,函数调用是编译器屏障,这意味着编译器不会通过调用移动全局内存访问。对此的一个警告是编译器知道的功能,例如,内置函数,内联函数(记住IPO!)等。
因此理论上需要一个处理器内存屏障(除了编译器屏障)才能使其工作。但是,由于您正在调用读取和写入是更改全局状态的系统调用,因此我非常确定内核在实现这些内容时会在某处发出内存屏障。虽然没有这样的保证,所以理论上你需要障碍。
答案 2 :(得分:2)
基本规则是:编译器必须使全局状态出现与编码它完全相同,但是如果它可以证明给定函数不使用全局变量那么它可以实现算法以任何方式选择。
结果是传统编译器总是将另一个编译单元中的函数视为内存屏障,因为它们无法在这些函数中看到它们。越来越多的现代编译器正在发展“整个程序”或“链接时间”优化策略,这些策略打破了这些障碍,并且将导致编写不良的代码失败,即使它已经运行了好几年。
如果有问题的函数在共享库中,那么它将无法在其中看到,但是如果函数是由C标准定义的函数那么它不需要 - 它已经知道这个功能是什么 - 所以你也必须小心这些。请注意,编译器将不识别内核调用,但是插入编译器无法识别的内容(内联汇编程序或对汇编程序文件的函数调用)的行为将会创造一个记忆障碍本身。
在你的情况下,notify
将是编译器无法看到的黑盒子(库函数),否则它将包含可识别的内存屏障,因此你很可能是安全的。
在实践中,你必须编写非常错误代码才能落实这一点。
答案 3 :(得分:1)
在实践中,他是正确的,并且在这种特定情况下隐含了记忆障碍。
但重点是,如果它的存在是“有争议的”,那么代码已经过于复杂和不清楚。
真的很棒,使用互斥或其他适当的构造。这是处理线程和编写可维护代码的唯一安全方法。
也许你会看到其他错误,比如如果send()被多次调用,代码是不可预测的。