【问题标题】:How to test the behavior of std::memory_order_relaxed?如何测试 std::memory_order_relaxed 的行为?
【发布时间】:2019-02-11 11:15:40
【问题描述】:

我已经阅读了std::memory_order_relaxed的文档。

Relaxed ordering的部分解释是......

// Thread 1:
r1 = y.load(memory_order_relaxed); // A
x.store(r1, memory_order_relaxed); // B
// Thread 2:
r2 = x.load(memory_order_relaxed); // C 
y.store(42, memory_order_relaxed); // D

这就是说的解释……

[It] 可以产生r1 == r2 == 42。特别是,如果由于编译器重新排序或在运行时,线程 2 中的 D 在 C 之前完成,则可能会发生这种情况。

我已经理解了解释,并尝试在我的电脑上测试如下代码:

std::atomic<int> x = {0};
std::atomic<int> y = {0};

int r1, r2;

void task1() {
    // Thread 1:
    r1 = y.load(memory_order_relaxed); // A
    x.store(r1, memory_order_relaxed); // B
}

void task2() {
   // Thread 2:
    r2 = x.load(memory_order_relaxed); // C 
    y.store(42, memory_order_relaxed); // D
}


int main()
{
    std::thread t2 (task2);
    std::thread t1 (task1);

    t1.join();
    t2.join();

    cout << "r1: " << r1
        << "\nr2: " << r2 << endl;

    return 0;
}

这段代码的结果是never r1 == r2 == 42,据说这是该文档中的一种可能行为。

这段代码有什么问题吗?或者,是不是有什么误会?

【问题讨论】:

  • 我明天中奖1000000是可能。但这可能。我可以尝试一生,永远不会中奖。 有人会赢。
  • x86 上的内存排序至少是获取释放。所以你不会在经典计算机上看到r1==r2==42。也许如果你以 ARM CPU 为目标,你可以看到它。
  • @Olive 这并不完全正确。 硬件 永远不会重新排序这些操作,但允许编译器根据语言规范重新排序它们(在不同的原子上)。观察到的行为取决于编译器是否实际上这样做。
  • @Oliv:也在多插槽设置上?您通常具有有限的非统一内存访问;每个内存模块只直接连接到一个 CPU。
  • @ArneVogel 理论上是的。但直到今天,所有编译器都将原子视为也被声明为 volatile。例如,编译器甚至不会优化存储在同一个函数内执行的单个原子上的两个连续的,即使标准允许这样做。

标签: c++ multithreading stl atomic memory-barriers


【解决方案1】:

或者,有什么误会吗?

是的,有一个。 std::memory_order_relaxed 在您的程序中允许的是针对架构的实现(编译器),以生成可能观察到副作用 r1 == r2 == 42 的程序。

一个实现不必产生这样的程序,这样的程序也不必产生​​那种副作用;无论如何,这是一个可能的结果。

如何测试 std::memory_order_relaxed 的行为?

我看不到这个问题的一般解决方案。您只能检查 you 观察到的副作用是否与 std::memory_order_relaxed 的规范匹配。

【讨论】:

  • 在高级实现而不是低级(如编译器)中使用 memory_order_relaxedmemory_order_seq_cst 的结果似乎相同。那么,什么时候应该真正使用 memory_order_relaxed 呢?或者,一般来说,在高层使用默认的memory_order(seq_cst)就可以了吗?
  • @eric_hsu 在您链接的页面中有很好的解释:库中所有原子操作的默认行为提供了顺序一致的排序(参见下面的讨论)。该默认值可能会损害性能,但可以为库的原子操作提供一个额外的 std::memory_order 参数,以指定编译器和处理器必须为该操作强制执行的原子性之外的确切约束。
  • @eric_hsu 你可能在一个总是保证原子操作顺序一致性的架构上操作。或者您的各个线程的相对时间尚未恰好以导致您正在寻找的行为的方式发生。
【解决方案2】:

您的代码有点幼稚,因为当第二个线程启动时,第一个线程可能已经完成。线程需要真正并发地运行这些代码。

要使r1 == r2 == 42 为真,它需要将负载C 重新排序到存储D 之后,x86 目前不执行在存储后重新排序的负载,因此您可能永远不会观察到这种情况在此平台上重新排序(除非编译器将 C 重新排序为 D)。

另一方面,ARM 和 PowerPC 的内存模型较弱。请参阅Runtime memory ordering 表。

【讨论】:

  • 这是不精确的,原因与 Oliv 之前的评论相同。在问题的评论线程中查看我的回复。
  • @ArneVogel 我认为您的评论不准确。特别是,硬件永远不会重新排序这些操作在 PowerPC 和 ARM 上是错误的。
  • 上下文很重要……我回应了 Oliv 关于 x86 的声明。然而,关键是 C++ 实现(即在这种情况下,通常是编译器)也可以重新排序不同原子变量上的宽松操作,即使硬件保证获取加载和释放存储,除非使用其他同步机制。 IOW,r1 == r2 == 42 实际上在 x86 上是可能的。
  • @ArneVogel 编译器可以重新排序CD,是的。但我看不出它为什么会在这里这样做的原因。编译器进行这种重新排序一定是有原因的(例如更快的代码、寄存器压力)。
  • 确实,这里没有做或不做的理由。该标准只是允许它,但通常它允许在实践中实际上不会发生的几种原子行为。一个极端的例子是臭名昭著的"out-of-thin-air" loads,不鼓励(“应该”与“应该”/“必须”)但不被禁止。
猜你喜欢
  • 2021-01-26
  • 2017-06-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-02-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多