如何解决多线程原子 a b 为 memory_order_relaxed 打印 00
在下面的代码中,对 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”。
- 在第二种情况下为什么不打印 00 ?怎么写到 x 没有存储在两个线程的 cpu 缓存中,然后打印 00 ?
- 它与 x86_64 有什么关系,但它在 arm/arm64/power 上打印 00 吗?
- 如果 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)
在同一线程中排序 a.load
之前(在 foo 和 bar 中),因此两个加载都必须看到该存储结果(或另一个稍后的存储值) . 这使得任何一个负载都无法看到初始的 0。
线程总是看到它自己按程序顺序执行的操作,即使它们是在使用 mo_relaxed
的原子对象上。这基本上等同于确保存储和加载发生在 asm(按程序顺序),但没有任何额外的障碍来防止其他线程观察到运行时重新排序,like if you'd used volatile
. (But don't)。乱序执行的基本原则是“不要破坏单线程代码”。
顺便说一句,您实际上是正确的,该值可以直接从 forwarded 中获得 store buffer,然后才能命中 L1d 缓存并变得全局可见。 (因为您没有使用 mo_seq_cst
;在先前的 seq_cst 存储全局可见之前,不会发生 seq_cst 加载。例如,在 x86 上,它必须编译为 xchg
存储,或 mov
+ mfence
。半相关:Globally Invisible load instructions 关于 x86 上加载结果的来源,尽管关于存储转发的一般观点适用于大多数主流 CPU,包括大多数 ARM。)
因此在实践中,负载很可能会看到自己线程存储的 1
,而不是来自其他线程的 1
,因为它会编译为允许存储转发到的 asm负载,并且负载紧随其后,因此它可能已经在执行和等待存储数据被转发的过程中,然后其他线程的存储在它们之间有任何窗口可见,除非在存储之间到达中断并加载。
例如,您可以通过存储 1
和 2
进行检查,看看您是否总是得到 12
或有时得到 21
。
您对为什么使用 2 个变量可以在您的版本中看到 00
的分析非常草率。
在下面的代码中,对 foo 中 a 的写入存储在存储缓冲区中,对 bar 中的 ra 不可见。同样,在 bar 中写入 b 对 foo 中的 rb 不可见
是的,存储缓冲区是 StoreLoad 重新排序的正常原因,并且 如果 foo
和 bar
几乎同时执行,那么是的,两个加载都可以在任一存储可以将自己提交到 L1d 缓存之前,发生并获取旧值。所以如果确实发生了,那是因为存储缓冲区。
但是存储缓冲区总是试图尽可能快地耗尽自己,并将待处理的存储提交到 L1d,在那里它们是全局可见的。这就是为什么很少真正看到 00
。通常,一个内核将获得缓存行的独占所有权并在另一个内核的加载可以运行之前提交其存储。
对 a
的写入将对另一个线程中的负载不可见,这绝对不是真的。它可能会也可能不会发生。
(半相关:Store Load 重新排序是性能“最重要”的一种,也是阻塞成本最高的一种。例如 x86 asm always 阻塞其他类型,即程序-order + store-forwarding,所以重新排序 2 个 store 的东西只能在 x86 上的编译时发生。How does memory reordering help processors and compilers?)
,在下面的代码中,对 foo 中 a 的写入存储在存储缓冲区中,对 bar 中的 ra 不可见。同样,在 bar 中写入 b 对 foo 中的 rb 不可见
这些都不是真的。 bar
是否看到 a
写入 foo
的值是不确定的,但并非未定义。同一内存上的所有原子操作都按照由实现确定的全局顺序执行。 bar
可以将 a
视为 0 或 1;要么是完全合法的,放松的记忆顺序对此没有任何改变。
在第二种情况下为什么不打印 00 ?为什么对 x 的写入不存储在两个线程的 cpu 缓存中,然后打印 00 ?
如前所述,同一内存上的所有原子操作都以某种全局顺序执行。此外,同一线程中同一内存上的所有操作都按程序顺序执行(即:不重新排序)。因此,在同一个线程中,您的 a.load
将检索刚刚设置为 a
的值或某个其他线程设置为 a
的值。它永远不会只是跳过该线程中的初始存储。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。