首页 > 解决方案 > 了解内存序列和 std::memory_order_relaxed

问题描述

我正在研究 C++ 内存序列,但这很令人困惑。

例如:

void sumUp(std::atomic<int>& sum, std::vector<int>& val)
{
   int tmpSum = 0;
   for(auto i = 0; i < 100; ++i) tmpSum += val[i];

   sum.fetch_add(tmpSum, std::memory_order_relaxed);
}

我不明白,sum.fetch_add()tmpSum += val[i]. 既然不按顺序,sum.fetch_add()之前可以操作tmpSum += val[i]吗?

那么总和0可能吗?

非常感谢。

标签: c++c++11c++14c++17

解决方案


memory_order在单个线程的上下文中没有可观察到的效果:

让我们看看 (xa最初b0):

auto t1(std::atomic<int>& x, int& a, int& b)
{
    a = 3;                                       // 1
    b = 5;                                       // 2
    x.store(a, std::memory_order_relaxed);       // 3
}

因为 (1) 和 (2) 不相互依赖,编译器可以重新排序它们。例如可以做 (1) -> (2) 或 (2) -> (1)

因为 (3) 取决于 (1) ((1) 写入a和 (3) 读取a),所以编译器不能在 (1) 之前执行 (3)。这与 (3) 中指定的内存顺序无关

因为 (3) 不依赖于 (2),通常在单线程模型中,编译器可以在 (2) 之前执行 (3)。

但是由于x是原子的,请考虑另一个线程执行此操作(x,a并且b是对与提供给的相同参数的引用,t1并且都是最初的0):

auto t2(std::atomic<int>& x, int& a, int& b)
{
    while(x.load(std::memory_order_relaxed) == 3)  // 4
        assert(b == 5);                            // 5
}

该线程一直等到xis 3,然后断言bis 5。现在您可以看到在顺序单线程世界中 (2) 和 (3) 如何在没有任何可观察行为的情况下重新排序,但在多线程模型中,(2) 和 (3) 的顺序可能会对行为产生影响的程序。

这就是这样memory_order做的:它指定在原子之前或之后重新排序的操作是否可以重新排序,而不会对单个线程产生任何影响。原因是它们可能对多线程程序产生影响。编译器无法知道这一点,只有程序员知道,因此有额外的memory_order参数。

使用memory_order_relaxed断言可能会失败,因为 (2) 可能发生在 (3) 之后,但使用memory_order_seq_cst(默认) 断言将永远不会失败,因为(2)发生在 (3) 之前。


回到您的示例,无论 memory_order您指定什么,都可以保证tmpSum += val[i];之前会发生,sum.fetch_add(tmpSum, std::memory_order_relaxed);因为第二个取决于第一个。这memory_order将影响不影响原子操作的指令的可能重新排序。例如,如果你有一个int unrelated = 24.


顺便说一句,官方术语是“sequenced before”和“sequenced after”


在现实世界中,硬件使事情变得更加复杂。操作可以在当前线程中以一种顺序出现,但另一个线程可以以另一种顺序看到它们,因此严格memory_order的 s 必须采用额外的措施来确保跨线程的顺序一致。


严格来说,在这个例子中,如果使用memory_order_relaxed我们将有未定义的行为,因为访问b不是跨线程同步的。


推荐阅读