【问题标题】:If one thread writes to a location and another thread is reading, can the second thread see the new value then the old?如果一个线程写入一个位置而另一个线程正在读取,那么第二个线程能否看到新值而不是旧值?
【发布时间】:2014-10-02 18:18:17
【问题描述】:

从 x = 0 开始。请注意,以下任何代码中都没有内存屏障。

易失性 int x = 0

线程 1:

while (x == 0) {}
print "Saw non-zer0"
while (x != 0) {}
print "Saw zero again!"

线程 2:

x = 1

是否有可能在任何(真实的)CPU 上看到第二条消息“又看到零了!”?在 x86_64 上呢?

同样,在这段代码中:

易失性 int x = 0。

线程 1:

while (x == 0) {}
x = 2

线程 2:

x = 1

x 的最终值是否保证为 2,或者 CPU 缓存是否可以以任意顺序更新主内存,这样虽然 x = 1 进入 CPU 的缓存,线程 1 可以看到它,但线程 1 被移动到另一个 cpu,它将 x = 2 写入该 cpu 的缓存,并且 x = 2 在 x = 1 之前被写回主内存。

【问题讨论】:

  • 在 C 语言的上下文中说:由于这是一场数据竞争,而数据竞争是未定义的行为,那么任何事情都可能发生。试图推理它真的没有多大意义。
  • 没有编程语言就无法回答(无论是 x86 指令)。
  • 在大多数语言中,编译器可以合法地将print(x); print(x); 重写为old = x; print(x); print(old);。所以大多数语言的答案是肯定的。
  • @MichaelBurr 在 C 语言的上下文中,共享对象上只有 volatile 操作,根据 ABI,这些操作必须准确执行;这是可观察行为的一部分。因此,当读取和写入符合 ABI(相应对齐)的 int 对象时,我们完全拥有 CPU 提供的语义。没有 UB,只有 ABI 和 CPU 特定的行为。
  • @usr 在 C 和 C++ 中,volatile 的意思是:在 asm 中按照确切的顺序执行这些内存操作。并且不要优化任何此类操作,即使它可能看起来是多余的。

标签: multithreading consistency cpu-cache


【解决方案1】:

是的,完全有可能。例如,编译器可能刚刚将x 写入内存,但仍将值保存在寄存器中。一个while 循环可以检查内存,而另一个则检查寄存器。

由于 CPU 缓存不会发生这种情况,因为缓存一致性硬件逻辑使缓存在您可能实际使用的所有 CPU 上都不可见。

理论上,您所说的写入竞争可能是由于发布的写入缓冲和读取预取而发生的。为了避免破坏遗留代码,我们使用了神奇的技巧使 x86 CPU 上的这种情况变得不可能。但是你不应该期望未来的处理器会这样做。

【讨论】:

  • 谢谢。您是否有任何详细讨论此问题的参考资料或示例代码/博客文章?另外,我将更新问题以使用 volatile 变量来解决注册问题;这更像是我最初的意图。
【解决方案2】:

抛开编译器完成的第二个技巧(即使是语言标准允许的技巧),我相信您是在问微架构在这种情况下如何表现。请记住,代码很可能会扩展为 cmp [x] + jz 或类似内容的繁忙等待循环,其中隐藏了负载。这意味着 [x] 很可能存在于核心运行线程 1 的缓存中。

在某个时候,线程 2 会来执行存储。如果它驻留在不同的核心上,则该行将首先从第一个核心完全失效。如果这些是在同一个物理内核上运行的 2 个线程 - 存储将立即影响所有按时间顺序较年轻的负载。

现在,现代无序机器上最有可能发生的事情是,此时管道中的所有负载都将是同一第一个循环的不同迭代(因为任何分支预测器都面临如此多的重复“采取”解决方案可能假设分支将继续被采用,直到被证明是错误的),所以会发生的情况是第一次加载遇到另一个线程修改的新值将导致匹配的分支简单地从所有年轻的操作,没有第二个循环有机会执行。

但是,由于某种原因,您可能确实进入了第二个循环(假设预测器在循环条件检查看到新值的正确时刻发出未采用的预测) - 在这种情况下,问题归结为这种情况:

  Time -->                                        
  ----------------------------------------------------------------
thread 1

    cmp [x],0                                            execute
    je ...                                                  execute (not taken)
    ...
    cmp [x],0        execute
    jne ...              execute (not taken)
  Can_We_Get_Here:
    ...

thread2 

    store [x],1                          execute

换句话说,鉴于大多数现代 CPU 可能会乱序执行指令,是否可以先评估较年轻的负载,然后再将较旧的负载评估到相同的地址,从而允许存储(来自另一个线程)更改值,以便它可以负载观察不一致。

我的猜测是,鉴于当今无序执行引擎的性质,上述时间线很有可能,因为它们只是仲裁并执行任何准备好的操作。然而,在大多数 x86 实现中,都有保护措施来防止这种情况,因为内存排序规则严格 say -

8.2.3.2 Neither Loads Nor Stores Are Reordered with Like Operations

此类机制可以检测到这种情况并刷新机器以防止陈旧/错误的值变得可见。所以答案是 - 不,这应该是不可能的,除非软件或编译器当然改变了代码的性质以防止硬件注意到这种关系。再说一次,内存排序规则有时是不稳定的,我不确定所有 x86 制造商都遵守完全相同的措辞,但这是一个非常基本的一致性示例,所以如果其中一个人错过了它,我会感到非常惊讶。

【讨论】:

    【解决方案3】:

    答案似乎是,“这正是 CPU 缓存一致性的工作。” x86 处理器实现了MESI protocol,这保证了第二个线程不能看到新值而不是旧值。

    【讨论】:

    • 缓存一致性本身并不能保证任何内存排序模型。 MESI 协议没有说明如何对来自两个不同上下文的读取/写入进行排序,它只保证无论它们形成什么顺序,总是读取最近的值(即使是由另一个内核或套接字写入)。跨度>
    猜你喜欢
    • 1970-01-01
    • 2018-03-26
    • 1970-01-01
    • 2015-11-02
    • 1970-01-01
    • 2012-09-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-08
    相关资源
    最近更新 更多