如何理解 std::memory_order (C++) 中的 RELAXED ORDERING

sky*_*ree 5 c++ atomic memory-model

我已经阅读了很多帖子并观看了几个 Youtube 视频 C++ 原子和内存模型(ConCpp 17、14)。

当我阅读Concurrency In Action 5.3.3 节RELAXED ORDERING一书时,我仍然无法理解作者在他的假设下提供的示例。

作者的假设

不仅仅是编译器可以重新排序指令。即使线程正在运行相同的代码位,由于在没有显式排序约束的情况下在其他线程中进行操作,它们可能会在事件顺序上产生分歧,因为不同的 CPU 缓存和内部缓冲区可以为相同的内存保存不同的值. 这很重要,我再说一遍:线程不必就事件的顺序达成一致。您不仅必须抛弃基于交错操作的心智模型,还必须抛弃基于编译器或处理器对指令重新排序的想法的心智模型。

假设我们看到的代码没有重新排序

示例代码:

#include <atomic>
#include <thread>
#include <assert.h>

std::atomic<bool> x,y;
std::atomic<int> z;

void write_x_then_y()
{
    x.store(true,std::memory_order_relaxed); // 1
    y.store(true,std::memory_order_relaxed); // 2
}

void read_y_then_x()
{
    while(!y.load(std::memory_order_relaxed)); // 3
    if(x.load(std::memory_order_relaxed))      // 4
        ++z;
}

int main() {
    x=false;
    y=false;
    z=0;

    std::thread a(write_x_then_y);
    std::thread b(read_y_then_x);
    a.join();
    b.join();

    assert(z.load()!=0); // 5
}
Run Code Online (Sandbox Code Playgroud)

从这个链接:https : //www.developerfusion.com/article/138018/memory-ordering-for-atomic-operations-in-c0x/

在此处输入图片说明

为什么x.load(relaxed)返回falsey.load(relaxed)返回true

作者的结论

这次断言 (5) 可以触发,因为 x (4) 的负载可以读取 false,即使 y (3) 的负载读取为 true 并且x (1) 的存储发生在 y (2) 的存储之前)。x 和 y 是不同的变量,因此没有与每个操作产生的值的可见性相关的排序保证。

问:为什么 x 的负载可以是假的?

作者得出结论,assert 可以触发。所以,z可以0。所以,if(x.load(std::memory_order_relaxed)):x.load(std::memory_order_relaxed)false

但无论如何,while(!y.load(std::memory_order_relaxed));使y true.

如果我们不重新排序(1)和(2)的代码序列,怎么可能y是真的但x仍然没有存储?

如何理解作者提供的图?

基于the store of x (1) happens-before the store of y (2),如果x.store(relaxed)发生在之前y.store(relaxed)x应该是true现在。但是,为什么x还是false甚至ytrue

Mik*_*ine 9

你和你的朋友都同意x=falsey=false。有一天,你给他写了一封信告诉他x=true。第二天你给他写了一封信告诉他y=true。你肯定会按正确的顺序给他寄信。

过了一段时间,你的朋友收到你写的一封信y=true。现在你的朋友知道什么x?他大概已经收到了告诉他的信x=true。但也许邮政系统暂时丢失了它,他明天就会收到。所以对他来说,当他收到这封信时x=falsex=true都是有效的可能性y=true

所以,回到硅的世界。线程之间的内存根本无法保证来自其他线程的写入以任何特定顺序出现,因此“延迟 x”完全有可能。所有添加atomic和使用relaxeddo 都是阻止两个线程在单个变量上竞争成为未定义的行为。它对订购不做任何保证就是强排序的用途。

或者,以更粗略的方式,看看我的 MSPaint 技能:

在此处输入图片说明

在这种情况下,作为“x”从第一个线程到第二个线程的流动的紫色箭头来得太晚了,而绿色箭头(y 交叉)发生得很快。

  • 会推荐 https://preshing.com/archives/ 用于他关于内存排序的博客 - 那里有 5-6 篇关于排序的文章,这是他几年来写的。可能需要时间来追踪它们,但非常值得。在该页面上搜索“订单”,然后从最旧的开始阅读。 (2认同)

use*_*ser 9

如果我们不重新排列(1)和(2)的代码序列,怎么可能y为真但x仍然没有被存储呢?

答案部分在第一句话中:

因为不同的CPU缓存和内部缓冲区可以为同一内存保存不同的值。

换句话说,其他线程可以看到过时的值。因此 x 和 y 可以被存储,但尚未传播到其他线程。并且缓存传播顺序可能与其存储顺序不同。

例如,三个线程,第一个修改 x 和 y,缓存以不同的顺序传播到不同的线程:

 x == 0          x == 0          x == 0 
 y == 0          y == 0          y == 0
+-------+       +-------+
| x = 1 | ----> | x = 1 |
+-------+       +-------+
+-------+                      +-------+
| y = 1 | -------------------> | y = 1 |
+-------+                      +-------+
  x == 1         x == 1          x == 0
  y == 1         y == 0          y == 1
Run Code Online (Sandbox Code Playgroud)