在我审查的代码库中,我发现了以下用法。
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在两个不同的处理器上运行-这种方案可能会失败。”这位资深程序员笑了,缓慢地解释道,就像教他五岁儿子如何系鞋带一样:“听着,小伙子,我们已经在这里看到了许多与线程相关的 bug,在高负载测试中和在真正的客户端中”,他停顿了一下,抚摸着自己略长的胡须,“但我们从来没有遇到过这种习语的 bug”。
“但是,书上说......”
“安静!”他迅速制止了我,“也许理论上来说,这并不是保证的,但实际上,你使用函数调用事实上就是一个内存屏障。编译器不会重新排序指令
global.data = data
,因为它无法知道是否有人在函数调用中使用它,而 x86 架构将确保其他 CPU 在线程B从管道读取命令时看到这个全局数据片段。请放心,我们已经有足够的真实世界问题需要担心了。我们不需要在虚假的理论问题上投入额外的精力。”“请放心,我的孩子,你会在未来理解如何把真正的问题与无需解决的问题分开。”
他是正确的吗?在实践中(比如x86、x64和ARM),这确实不是一个问题吗?
这违反了我所学的一切,但他有着长长的胡须和非常聪明的外表!
如果您能向我展示一段代码证明他是错误的,那就更好了!