函数调用是现代平台的有效内存障碍吗?

mik*_*och 66 c multithreading memory-barriers

在我评论的代码库中,我发现了以下习语.

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;
    ...
}
Run Code Online (Sandbox Code Playgroud)

"抓住它",我对作者说,我的团队的一名高级成员,"这里没有内存障碍!你不能保证global.data将从缓存刷新到主内存.如果线程A和线程B将运行两个不同的处理器 - 这个方案可能会失败".

高级程序员咧嘴一笑,慢慢解释,仿佛在解释他五岁的男孩如何系鞋带:"听小男孩,我们在这里看到很多线程相关的错误,在高负载测试中,在真实客户中",他他停下来留下他长长的胡须,"但我们从来没有这个成语的错误".

"但是,它在书中说......"

"安静!",他立刻叫醒我,"也许理论上,它不能保证,但实际上,你使用函数调用的事实实际上是一个内存屏障.编译器不会重新排序指令global.data = data,因为它无法知道是否任何人在函数调用中使用它,并且x86架构将确保其他CPU在线程B从管道读取命令时将看到这段全局数据.请放心,我们有充足的现实问题需要担心.我们不需要在虚假的理论问题上投入额外的努力.

"请放心,我的孩子,你会理解将真正的问题与我需要获得博士的非问题分开."

他是对的吗?这在实践中真的不是问题(比如x86,x64和ARM)吗?

这是我所学到的一切,但他确实有一个长胡子和一个非常聪明的外观!

额外的积分如果你能告诉我一段代码证明他错了!

Mah*_*dsi 11

内存障碍不仅仅是为了防止指令重新排序.即使指令没有重新排序,它仍然可能导致缓存一致性问题.至于重新排序 - 它取决于您的编译器和设置.ICC对重新排序特别激进.MSVC w /整个程序优化也可以.

如果您的共享数据变量被声明为volatile,即使它不在规范中,大多数编译器将生成围绕变量读取和写入的内存变量,并防止重新排序.这不是正确的使用方式volatile,也不是它的用途.

(如果我还剩下任何选票,我会为你的叙述+1.)

  • 但这真的是`x86` /`x64`中的一个问题.我可以写一个证明它失败的简短程序吗?(谢谢你的客气话,技术讨论应该很有趣). (2认同)
  • @mike他们尝试*不*使用Itanic,我们都知道他们在那里取得的成功。然后AMD出现了,并说“这是一个64位平台,它将运行您的x86二进制文件,并且*运行您为x64重新编译的糟糕代码,而不会解决您的错误”,因此x86_64诞生了。 (2认同)

jan*_*neb 9

实际上,函数调用是编译器屏障,这意味着编译器不会通过调用移动全局内存访问.需要注意的是编译器知道的功能,例如内置函数,内联函数(请记住IPO!)等.

因此理论上需要一个处理器内存屏障(除了编译器屏障)才能实现这一功能.但是,由于您正在调用读取和写入是更改全局状态的系统调用,因此我非常确定内核在实现这些内容时会在某处发出内存屏障.虽然没有这样的保证,所以理论上你需要障碍.

  • 那么在实践中,内核模式代码==内存屏障?听起来很合理,听起来这位老人毕竟是对的.听起来ICC无法对系统调用周围的代码进行重新排序,因为他不知道内核会做什么. (3认同)
  • 这实际上是对原始问题的唯一直接答案。答案是:不。函数调用始终是编译器障碍。但函数调用不能保证是内存屏障。仅当被调用函数中的代码包含内存屏障时才会出现这种情况。 (2认同)