【问题标题】:output 10 with memory_order_seq_cst使用 memory_order_seq_cst 输出 10
【发布时间】:2021-06-24 15:40:29
【问题描述】:

当我运行这个程序时,我得到的输出是 10,这对我来说似乎是不可能的。我在 x86_64 core i3 ubuntu 上运行它。

如果输出是 10,那么 1 肯定来自 c 或 d。

同样在线程 t[0] 中,我们将 c 赋值为 1。现在 a 为 1,因为它出现在 c=1 之前。 c 等于 b,线程 1 将其设置为 1。所以当我们存储 d 时,它应该是 1,因为 a=1。


  • 输出 10 可以与 memory_order_seq_cst 一起发生吗?我尝试在第 1 行(变量 =1 )和第 2 行(printf)之间的两个线程上插入 atomic_thread_fence(seq_cst) 但它仍然没有工作。

取消注释 fence 不起作用。 尝试使用 g++clang++ 运行。两者都给出相同的结果。

#include<thread>
#include<unistd.h>
#include<cstdio>
#include<atomic>
using namespace std;

atomic<int> a,b,c,d;

void foo(){
        a.store(1,memory_order_seq_cst);
//        atomic_thread_fence(memory_order_seq_cst);
        c.store(b,memory_order_seq_cst);
}

void bar(){
        b.store(1,memory_order_seq_cst);
  //      atomic_thread_fence(memory_order_seq_cst);
        d.store(a,memory_order_seq_cst);
}

int main(){
        thread t[2];
        t[0]=thread(foo); t[1]=thread(bar);
        t[0].join();t[1].join();
        printf("%d%d\n",c.load(memory_order_seq_cst),d.load(memory_order_seq_cst));
}
bash$ while [ true ]; do ./a.out | grep "10" ; done 
10
10
10
10

【问题讨论】:

  • 你调试/设置断点和单步吗?
  • @StureS 这是一个线程问题,设置断点和单步执行会改变程序的行为。
  • 这应该会给你一个线索,问题出在哪里。请参阅下面 Dani 的答案。
  • @RichardCritten:公平地说,分别设置断点和单步执行每个线程可以让您探索可能的排序。但不是运行时 re 存储缓冲区和飞行中的多个请求的排序效果;单步执行非常慢,以至于在编译器确定了它选择的任何编译时顺序之后,每个 asm 操作实际上都是 seq_cst。对于无锁队列之类的设计进行健全性检查可能是一种有用的技术,但可能不适用于此类。 (虽然一切都已经是 seq_cst 所以也许)
  • @nvn:您的文字描述仍在讨论每个线程函数内部的 printfs,而不是在两个加入之后。如果您打印了d=%d\n 或其他任何方式,则任何一种方式都是等效的;这种方式确实可以明确哪个数字来自哪个变量。

标签: c++ multithreading memory-barriers stdatomic lockless


【解决方案1】:

printf 语句是不同步的,因此 10 的输出可能只是重新排序的 01。
01 发生在 printf 之前的函数串行运行时。

【讨论】:

  • 我不认为这是由于 printf。所以我删除了 printf 并请查看更新的程序,我将值存储在原子变量中并在加入两个线程后打印它们。
【解决方案2】:

10 (c=1, d=0) 很容易解释:bar 恰好先运行,在foo 读取b 之前完成。

在不同内核上启动线程的内核间通信的怪癖意味着即使thread(foo) 在主线程中首先运行,这种情况也很容易发生。例如也许一个中断到达了操作系统为foo 选择的核心,延迟了它实际进入该代码1

请记住,seq_cst 仅保证所有 seq_cst 操作存在某种总顺序,该顺序与每个线程中的先序顺序兼容。 (以及由其他因素建立的任何其他发生之前的关系)。因此,以下原子操作顺序是可能的,甚至无需将 bar 中的 a.load2 与生成的 int 临时的 d.store 分开。

        b.store(1,memory_order_seq_cst);   // bar1.  b=1
        d.store(a,memory_order_seq_cst);   // bar2.  a.load reads 0, d=0

        a.store(1,memory_order_seq_cst);   // foo1
        c.store(b,memory_order_seq_cst);   // foo2.  b.load reads 1, c=1
// final: c=1, d=0

atomic_thread_fence(seq_cst) 在任何地方都没有影响,因为您的所有操作都已经是seq_cst 栅栏基本上只是停止重新排序该线程的操作;它不会等待或与其他线程中的栅栏同步。

(只有看到另一个线程存储的值的负载才能创建同步。但是这样的负载不会等待另一个存储;它无法知道还有另一个存储。如果你想继续加载直到你看到你期望的值,你必须编写一个旋转等待循环。)


脚注 1: 由于您所有的原子变量可能都在同一个缓存行中,即使执行确实在两个不同的内核上同时到达 foobar 的顶部,错误共享也可能会让两个操作都来自一个线程在另一个核心仍在等待获得独占所有权时发生。尽管 seq_cst 存储足够慢(至少在 x86 上),但硬件公平的东西可能会在提交1 的第一个存储后放弃独占所有权。无论如何,一个线程中的两个操作在另一个线程之前发生并获得 10 或 01 的方式有很多。如果我们在任一加载之前获得b=1 然后a=1,甚至可能获得11 .使用 seq_cst 确实会阻止硬件提前加载(在存储全局可见之前),所以这是很有可能的。

脚注 2:裸 a 的左值到右值评估使用重载的 (int) 转换,相当于 a.load(seq_cst)。来自foo 的操作可能发生在该负载和从中获取临时值的d.store 之间。 d.store(a) 不是原子拷贝;相当于int tmp = a;d.store(tmp);。这没有必要解释您的观察结果。

【讨论】:

  • a,b,c,d 都是原子的。如果 d.store(a) 不是原子副本,我应该用什么使它成为原子?能详细一点吗?
  • @nvn:你所拥有的是一个原子加载和一个单独的原子存储。 C++11(和大多数硬件)没有 2 操作数原子指令,所以你不能让它成为原子副本,除非使用锁/互斥锁来禁止任何其他线程观察非原子性。或者在一些 ISA 上,比如 m68k 的某些版本,硬件支持像 DCAS 这样的操作,它是一次对 2 个非连续内存操作的原子 RMW,但 C++ 没有公开它,否则几乎所有实现需要.is_lock_free() == false
  • @nvn:或者在某些现代 CPU 上,您可以使用事务内存将读取和写入操作合并为单个事务。 (例如,如果 Intel TSX 没有因为安全(信息泄漏侧通道)勘误原因而被禁用。)当然,在大多数用例中没有必要这样做,并且大多数观察者无法区分单独原子负载之间的区别和存储与单个原子事务。
猜你喜欢
  • 2017-12-24
  • 2017-08-22
  • 2018-08-04
  • 2012-09-02
  • 1970-01-01
  • 2018-02-21
  • 1970-01-01
  • 2019-10-06
  • 1970-01-01
相关资源
最近更新 更多