函数调用是现代平台的有效内存屏障吗?
在我回顾的代码库中,我find了下面的习语。
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
将从caching刷新到主内存。如果线程A和线程B将运行在两个不同的处理器上 – 这种scheme可能会失败“。
高级程序员咧嘴一笑,缓缓地解释道,仿佛在解释他的五岁男孩怎样系鞋带:“听小男孩,我们在这里看到很多与线程相关的bug,在高负载testing和真实客户端”,他他暂停了一下他那长长的胡须,“但是我们从来没有这个成语的错误”。
“但是,它在书中说…”
“挺!”,他立刻给我安静了一下,
“从理论上讲,并不能保证,在实践中,你使用函数调用的实际上是一个内存屏障,编译器不会重新排列指令global.data = data
,因为它不知道是否有人在函数中使用它调用,而x86架构将确保其他CPU将在线程B从pipe道读取命令的时候看到这段全局数据。请放心,我们有充足的现实世界问题需要担心,我们不需要在假的理论问题上投入额外的努力。
放心吧,我的孩子,你会明白的,将真正的问题从我需要的博士学位分开来解决。“
他是对的吗? 这在实践中是不是一个问题(比如说x86,x64和ARM)?
这与我学到的所有东西都是相反的,但是他确实有一头长长的胡子和一个真正漂亮的外表!
加分,如果你能给我看一段代码certificate他错了!
内存障碍不仅仅是为了防止教学重新sorting。 即使指令没有重新sorting,它仍然可能导致caching一致性问题。 至于重新sorting – 这取决于你的编译器和设置。 国际刑事法院对重新sorting尤其积极。 MSVC w /整个程序优化也可以。
如果您的共享数据variables被声明为volatile
, 即使它不在spec中,大多数编译器都会根据variables读写产生一个内存variables,并阻止重新sorting。 这不是使用volatile
的正确方式,也不是它的意思。
(如果我还有任何投票,我会对你的问题进行解说。)
实际上,函数调用是一个编译器障碍,意味着编译器不会移动全局内存访问超过调用。 对此的一个警告是编译器知道的一些function,例如内置函数,内联函数(记住IPO!)等。
所以处理器内存屏障(除了编译器屏障之外)理论上是需要做这个工作的。 但是,由于您正在调用读写系统调用来改变全局状态,所以我很确定内核在执行这些调用时会发出内存障碍。 虽然没有这样的保证,所以理论上你需要有障碍。
基本的规则是:编译器必须使全局状态看起来像你编写它一样,但是如果它能certificate一个给定的函数不使用全局variables,那么它可以按照它select的任何方式实现algorithm。
结果是传统的编译器总是把另一个编译单元中的函数作为内存屏障,因为他们看不到这些函数的内部。 越来越多的现代编译器正在制定“整体程序”或“链接时间”优化策略,这些策略打破了这些障碍,并且会导致编写不好的代码失败,尽pipe多年来一直运行良好。
如果有问题的函数在共享库中,那么它将无法在内部看到, 但是如果函数是由C标准定义的函数,那么它不需要 – 它已经知道函数的作用了, – 所以你也必须小心这些。 请注意,编译器不会识别内核调用的内容,但插入编译器无法识别的内容(内联汇编程序或调用汇编程序文件的函数)本身会创build内存屏障。
在你的情况下, notify
将是一个编译器无法看到的黑盒(一个库函数),否则它将包含一个可识别的内存屏障,所以你很可能是安全的。
在实践中,你必须编写非常糟糕的代码才能解决这个问题。
在实践中,他是正确的,在这个特定的情况下隐含着记忆障碍。
但重要的是,如果它的存在是“有争议的”,则代码已经太复杂,不清楚了。
真的,家伙,使用互斥或其他适当的构造。 这是处理线程和编写可维护代码的唯一安全方法。
也许你会看到其他的错误,比如如果send()被多次调用,代码是不可预知的。