多线程原子 a b 为 memory_order_relaxed 打印 00

Multithreading atomics a b printing 00 for memory_order_relaxed

在下面的代码中,对 foo 中的 a 的写入存储在存储缓冲区中,并且对 bar 中的 ra 不可见。同样,在 bar 中对 b 的写入对于 foo 中的 rb 是不可见的,它们打印 00.

// g++ -O2 -pthread axbx.cpp ; while [ true ]; do ./a.out | grep "00"; done prints 00 within 1min
#include<atomic>
#include<thread>
#include<cstdio>
using namespace std;
atomic<long> a,b;
long ra,rb;
void foo(){
        a.store(1,memory_order_relaxed);
        rb=b.load(memory_order_relaxed);
}
void bar(){
        b.store(1,memory_order_relaxed);
        ra=a.load(memory_order_relaxed);
}
int main(){
  thread t[2]{ thread(foo),thread(bar)};
  t[0].join();t[1].join();
  if((ra==0) && (rb==0)) printf("00\n"); // each cpu store buffer writes not visible to other threads.
}

下面的代码与上面的代码几乎相同,只是删除了变量 b 并且 foo 和 bar 都具有相同的变量 'a' 并且 return 值存储在 ra1 和 ra2 中。在这种情况下,我在 运行 5 分钟后至少不会得到“00”。

  1. 第二种情况为什么不打印00?怎么写到x 没有存储在两个线程的 cpu 缓存中然后打印 00 ?
  2. 它与 x86_64 有任何关系但它在 arm/arm64/power 上打印 00 吗?
  3. 如果 arm/arm64/power 打印 00 ,存储在 foo 和 bar 中的 smp_mb() 会修复它吗?
// g++ -O2 -pthread axbx.cpp ; while [ true ]; do ./a.out | grep "00"; done doesn't print 00 within 5 min
#include<atomic>
#include<thread>
#include<cstdio>
using namespace std;
atomic<long> a,b;
long ra1,ra2;
void foo(){
        a.store(1,memory_order_relaxed);
        ra1=a.load(memory_order_relaxed);
}
void bar(){
        a.store(1,memory_order_relaxed);
        ra2=a.load(memory_order_relaxed);
}
int main(){
  thread t[2]{ thread(foo),thread(bar)};
  t[0].join();t[1].join();
  if((ra1==0) && (ra2==0)) printf("00\n"); // each cpu store buffer writes not visible to other threads.
}

a.store(1, mo_relaxed) 在同一个线程中(在 foo 和 bar 中) a.load 之前排序,因此两个加载都必须看到存储结果(或另一个后来的存储值)。 这使得任何一个加载都不可能看到初始的 0。

一个线程总是按程序顺序看到它自己的操作,即使它们在原子对象上使用mo_relaxed. 这基本上等同于确保存储和加载发生在 asm 中(按程序顺序),但没有任何额外的障碍来防止 运行 其他线程观察到时间重新排序, like if you'd used volatile. (But don't)。乱序执行的基本规则是“不要破坏单线程代码”。


顺便说一句,尽管有关存储转发的一般观点适用于包括大多数 ARM 在内的大多数主流 CPU,但您实际上是正确的,关于 x86 上加载结果来自何处的值可以是 forwarded directly from the before it hits L1d cache and becomes globally visible. (Because you didn't use mo_seq_cst; seq_cst loads can't happen until previous seq_cst stores are globally visible. e.g. on x86 it would have to compile to an xchg store, or mov + mfence. Semi-related: 。)

所以在实践中,加载很可能会看到自己线程存储的 1,而不是其他线程的 1,因为它将编译为允许存储的 asm转发到负载,并且负载紧随其后,因此它可能已经在执行和等待转发存储数据的过程中,然后才能在它们之间看到任何 window 其他线程的存储,除非存储和加载之间出现中断。

你可以通过存储一个 1 和一个 2 来检查,例如,看看你是否总是得到 12 或有时得到 21.


您对为什么在您的版本中使用 2 个变量可以看到 00 的分析非常草率。

In the below code the write to a in foo is stored in store buffer and not visible to the ra in bar. Similarly the write to b in bar is not visible to rb in foo

是的,存储缓冲区是 StoreLoad 重新排序的正常原因,并且 if foobar 几乎同时执行,那么是的,两种加载都可能发生,并在任何一家商店将自己提交给 L1d 缓存之前获取旧值。因此,如果确实发生了这种情况,那是因为存储缓冲区。

但是存储缓冲区总是试图尽快耗尽自身并将待处理的存储提交到全局可见的 L1d。这就是为什么很少真正看到 00。通常一个核心将获得缓存行的独占所有权并在另一个核心的负载可以之前提交其存储 运行.

a 的写入将 对另一个线程中的负载不可见绝对不是真的。它可能会也可能不会发生。

(半相关:Store Load reordering is the "most important" one for performance, and the most expensive one to block. For example x86 asm always blocks the other kinds, being program-order + store-forwarding, so stuff reordering 2 stores wrt. each other can only happen at compile-time on x86.

In the below code the write to a in foo is stored in store buffer and not visible to the ra in bar. Similarly the write to b in bar is not visible to rb in foo

这些都不是真的。 bar 是否看到 foo 写入 a 的值是不确定的,但不是未定义的。同一内存上的所有原子操作都按照实现确定的某种全局顺序执行。 bar 可以将 a 视为 0 或 1;两者都是完全合法的,放松的记忆顺序对此没有任何改变。

In the second case why doesn't it print 00 ? How come writes to x are not stored in cpu cache for both threads and then print 00 ?

如前所述,同一内存上的所有原子操作都以某种全局顺序执行。此外,同一线程中同一内存上的所有操作均按程序顺序执行(即:无重新排序)。因此,在同一个线程中,您的 a.load 将检索刚刚设置为 a 的值或其他线程设置为 a 的值。它将从不 只是跳过该线程中的初始存储。