70

在我查看的代码库中,我发现了以下成语。

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 架构将确保在线程 B 从管道中读取命令时,其他 CPU 将看到这条全局数据。请放心,我们有很多现实世界的问题要担心。我们不需要在虚假的理论问题上投入额外的精力。

“放心,我的孩子,到时候你会明白将真正的问题与我需要获得博士学位的非问题区分开来。”

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

这与我学到的一切背道而驰,但他确实留着长胡子和非常聪明的外表!

如果你能告诉我一段代码证明他错了,加分!

4

4 回答 4

12

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

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

(如果我还有任何选票,我会为你的旁白问题 +1。)

于 2012-05-22T08:21:34.937 回答
10

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

因此,理论上需要一个处理器内存屏障(除了编译器屏障)来完成这项工作。但是,由于您正在调用 read 和 write ,它们是改变全局状态的系统调用,所以我很确定内核在这些实现的某个地方发出了内存屏障。虽然没有这样的保证,所以理论上你需要障碍。

于 2012-05-22T08:24:32.580 回答
2

The basic rule is: the compiler must make the global state appear to be exactly as you coded it, but if it can prove that a given function doesn't use global variables then it can implement the algorithm any way it chooses.

The upshot is that traditional compilers always treated functions in another compilation unit as a memory barrier because they couldn't see inside those functions. Increasingly, modern compilers are growing "whole program" or "link time" optimization strategies which break down these barriers and will cause poorly written code to fail, even though it's been working fine for years.

If the function in question is in a shared library then it won't be able to see inside it, but if the function is one defined by the C standard then it doesn't need to -- it already knows what the function does -- so you have to be careful of those also. Note that a compiler will not recognise a kernel call for what it is, but the very act of inserting something that the compiler can't recognise (inline assembler, or a function call to an assembler file) will create a memory barrier in itself.

In your case, notify will either be a black box the compiler can't see inside (a library function) or else it will contain a recognisable memory barrier, so you are most likely safe.

In practice, you have to write very bad code to fall over this.

于 2012-05-22T09:34:07.373 回答
2

在实践中,他是正确的,并且在这种特定情况下暗示了记忆障碍。

但关键是,如果它的存在是“有争议的”,那么代码已经太复杂和不清楚了。

真的,伙计们,使用互斥锁或其他适当的结构。这是处理线程和编写可维护代码的唯一安全方法。

也许你会看到其他错误,比如如果 send() 被多次调用,代码是不可预测的。

于 2012-05-28T16:32:12.997 回答