对c++内存顺序的理解,我错了吗?

Posted

技术标签:

【中文标题】对c++内存顺序的理解,我错了吗?【英文标题】:Understanding of c++ memory order,am I wrong? 【发布时间】:2019-11-20 12:06:40 【问题描述】:
#include <atomic>
#include <cassert>
#include <thread>

std::atomic<bool> x = false, y = false, go = false;
int v = 0;

// t1
void write_xy() 
  while (!go) 
    std::this_thread::yield();
  

  v = 1;                                     // 1
  x.store(true, std::memory_order_relaxed);  // 2
  y.store(true, std::memory_order_relaxed);  // 3


// t2
void read_yx() 
  while (!go) 
    std::this_thread::yield();
  

  while (!y.load(std::memory_order_relaxed))
    ;

  assert(1 == x.load(std::memory_order_relaxed));  // 4
  assert(1 == v);                                  // 5


int main() 
  for (;;) 
    x = false;
    y = false;
    v = 0;

    go = false;
    std::thread t1(write_xy);
    std::thread t2(read_yx);
    go = true;  // start
    t1.join();
    t2.join();
  

作为一个C++并发编程的初学者,根据我对memory_order_relaxed的理解,上面代码中t1线程中的三个语句的执行顺序对于t2是不可见的。从t2的角度来看,t1中的三个语句的顺序可能是3、2、1,所以4和5处的assert可能会触发。

经过多次尝试,assert 始终没有触发,所以我编写了一个无限循环重复上述过程,assert 仍然没有触发。后来怀疑t1t2开始执行之前就结束了,所以引入go变量在两个线程开始时等待,保证两个线程尽快开始执行,而assert仍然没有触发。

我在使用 Centos8 和 4 个 CPU 的虚拟机上进行测试。我的 CPU 是 i5-7500。

【问题讨论】:

AFAIK,x86-64 不支持memory_order_relaxed 商店的硬件。所有商店都是发布商店。在汇编级别,x.store(true, std::memory_order_relaxed)x.store(true, std::memory_order_release) 之间没有区别。 某事没有发生并不意味着它不会发生。有太多的参数在这里起作用。例如,IIRC、x64 无法重新排序加载-加载或存储-存储操作。因此,如果它们没有被编译器重新排序,它就不会在运行时发生。但这并不适用于所有架构。 【参考方案1】:

只是语言允许某些事情发生并不意味着您可以在给定的情况下重现它。

让我们暂时忽略v 上的数据竞争(即使这意味着您的程序有未定义的行为)。

您正在为 x86 编译代码,它对内置的内存顺序有非常强的保证。例如,当您使用 std::memory_order_release 执行存储时,您会得到完全相同的汇编代码:

https://godbolt.org/z/pZaFDC

    mov     DWORD PTR v[rip], 1
    mov     BYTE PTR x[rip], 1
    mov     BYTE PTR y[rip], 1

因此,当y == 1 时,保证所有其他线程都可以看到此代码(为您的 CPU 编译)v == 1x == 1。你的 C++ 程序没有这个保证,但是这个机器代码有。

同样,使用std::memory_order_acquire 进行加载没有任何效果(只有断言消息的文本发生变化):

https://godbolt.org/z/e2-uNA

    movzx   eax, BYTE PTR y[rip]
[...]
    movzx   eax, BYTE PTR x[rip]
[...]
    cmp     DWORD PTR v[rip], 1

同样,该平台已经提供了必要的保证。其他平台(例如 ARM)提供的保证较少,您会在编译后的二进制文件中看到差异:

https://godbolt.org/z/Ru4YdD

这里,同步被添加到所有的 store 并读取:

    bl      __sync_synchronize

上面的 x86 代码也是为什么 v 上的数据竞争此时没有效果的原因。 然而,依赖这个是一个糟糕的主意,因为编译器将完全拥有它的权利,例如在while (!y.load(std::memory_order_relaxed)) 之前移动assert(v == 1);。只是目前还没有这样做。

另一种获取断言的方法是编译器重新排序您的加载和存储。它会被允许这样做(而上面的发布-获取排序则不会),但它不会这样做,大概是因为这样做没有意义。您可以通过更改周围的代码来诱使它这样做,但我想不出办法来做到这一点。

【讨论】:

您可能需要添加一些 ARM 程序集来对比 x86。 @Evg 好主意。 非常感谢,有什么方法可以让我的编译器(gcc)重新排序吗?? @honghuibi gcc 允许重新排序原始代码中的加载和存储。它现在不会重新排序它们。我不知道如何让 gcc 重新排序它们。 @MaxLanghof,不确定我的信息是否过时,但 gcc 对原子访问几乎没有优化。

以上是关于对c++内存顺序的理解,我错了吗?的主要内容,如果未能解决你的问题,请参考以下文章

看起来 is_nothrow_constructible_v() 在 MSVC 中被破坏了,我错了吗?

并发编程-内存模型

深入理解Java内存模型——总结

深入理解Java内存模型——总结

c++你能猜出来哪写错了吗

深入理解Java内存模型——顺序一致性