【问题标题】:Multithreading atomics a b printing 00 for memory_order_relaxed多线程原子 a b 为 memory_order_relaxed 打印 00
【发布时间】:2021-07-01 00:51:37
【问题描述】:

在下面的代码中,对 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' 并且返回值存储在 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.
}

【问题讨论】:

  • 一般不要将 Linux smp_mb()std::atomic 混合使用,只需使用 atomic_thread_fence(std::memory_order_seq_cst) 或任何你想要的内存顺序。

标签: c++ multithreading memory-barriers stdatomic


【解决方案1】:

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

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


顺便说一句,您实际上是正确的,该值可以直接来自forwarded,然后它会到达 L1d 缓存并成为全局可见的。 (因为您没有使用mo_seq_cst;直到以前的 seq_cst 存储全局可见之前,seq_cst 加载才会发生。例如,在 x86 上,它必须编译为 xchg 存储,或 mov + mfence。半-related:Globally Invisible load instructions 关于 x86 上加载结果的来源,尽管关于存储转发的一般观点适用于大多数主流 CPU,包括大多数 ARM。)

所以在实践中,负载很可能会看到它们自己的线程存储的1,而不是来自其他线程的1,因为它会编译为允许存储转发到负载的asm,并且加载就在之后,因此它可能已经在执行并等待存储数据被转发之前,在它们之间有任何窗口可以让其他线程的存储可见,除非在存储和加载之间出现中断。

您可以通过存储12 来检查,例如,看看您是否总是得到12 或有时得到21


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

在下面的代码中,对 foo 中的 a 的写入存储在存储缓冲区中,并且对 bar 中的 ra 不可见。同样,bar 中的 b 写入对 foo 中的 rb 不可见

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

但是存储缓冲区总是试图尽可能快地耗尽自身,并将挂起的存储提交到全局可见的 L1d。这就是为什么很少见到00 的原因。通常一个核心将获得缓存行的独占所有权并在另一个核心的负载运行之前提交其存储。

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

(半相关:Store Load 重新排序对于性能而言是“最重要的”,也是最昂贵的阻塞。例如 x86 asm always 阻塞其他类型,即程序顺序+ 存储转发,所以重新排序 2 个存储的东西只能在 x86 的编译时发生。How does memory reordering help processors and compilers?)

【讨论】:

  • 我有一台 x86_64 机器,但我们能否以某种方式验证这些机器是否适用于不同的架构。我尝试了 qemu-static-aarch64, qemu-ppc64-static 但 qemu 文档说它将以底层 x86_64 的底层 cpu 顺序执行。所以我几乎无法验证这些。 gcc/clang tsan 也不检查 memory_order。这应该是另一个问题吗?
  • @nvn:不,所有 CPU 都会为您的第二版提供11,因为这是 ISO C++ 所要求的。
  • 我会问另一个问题。
  • @nvn:关于什么?我看不出还有什么问题没有解决。正如我在回答中所说(在最后一次编辑之后),乱序执行的基本规则是不破坏单线程执行。甚至第一个版本也提到了存储转发:这就是 CPU 如何在全局可见之前设法看到自己的存储,而不是让最近存储的每次重新加载都必须停止,直到存储缓冲区将其提交到 L1d 缓存。
【解决方案2】:

在下面的代码中,对 foo 中的 a 的写入存储在存储缓冲区中,并且对 bar 中的 ra 不可见。同样,bar 中的 b 写入对 foo 中的 rb 不可见

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

在第二种情况下,为什么不打印 00 ?为什么对 x 的写入没有存储在两个线程的 cpu 缓存中然后打印 00 ?

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-01-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-03-23
    • 1970-01-01
    • 2021-11-12
    相关资源
    最近更新 更多