< 返回版块

Rgoogle 发表于 2024-04-18 00:16

use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;

fn main() {
    let data = Arc::new(AtomicBool::new(false));

    let data_clone = Arc::clone(&data);

    let thread1 = thread::spawn(move || {
        data_clone.store(true, Ordering::Release);
    });

    let thread2 = thread::spawn(move || {
        if data.load(Ordering::Acquire) {
            println!("Data is true");
        } else {
            println!("Data is false");
        }
    });

    thread1.join().unwrap();
    thread2.join().unwrap();
}

以上的代码为什么多次运行结果不一致,那么如果这个有问题,那么是不是Arc等也存在同样的问题?

评论区

写评论
xiami 2024-04-21 07:25

首先这不是指令重排导致的, 而是thread1和thread2谁先执行的问题 指令重排只关系到当前线程

至于你想到达“控制多个线程之间的执行顺序”我推荐 Condvar

作者 Rgoogle 2024-04-20 17:07

总结了一下,是这个意思吧,感觉下面的评论也没有回答清楚,因为之前就是因为不知道如何控制,还有以为这个可以控制多个线程之间的执行顺序,实际上根本就控制不了

作者 Rgoogle 2024-04-20 17:06

问题的产生,因为我看到有文章说,编译器和cpu会修改我们的代码,调换顺序,导致代码执行顺序和源代码的执行顺序不一样。 比如以下的代码:

static X: AtomicI32 = AtomicI32::new(0);
static Y: AtomicI32 = AtomicI32::new(0);

fn a() {
    X.store(10, Relaxed); // 1
    Y.store(20, Relaxed); // 2
}

fn b() {
    let y = Y.load(Relaxed); // 3
    let x = X.load(Relaxed); // 4
    println!("{x} {y}");
}

这个代码执行会存在多种输出的情况,其中一种输出特别的奇怪。启动两个独立线程,分别执行a,b函数。 程序执行顺序是 2-> 3 -> 4 -> 1 (这里的数字表示上面源代码注释后面的数字,比如开始是2,说明程序执行的时候,先执行上面注释是2的那行代码) 输出的的结果是 0 20 ,也就是x是0,y是20.

你可能会非常的奇怪。凭什么2这条语句要比1先执行?你的代码明明1是在2的上面,程序按照重上往下执行,凭上面你2先执行呢?难道不应该是先执行1嘛?

这就是重点了。编译器或者cpu会认为这样加快程序的运行效率。所以这么干了。用户很烦恼,难道编译器就不怕这么修改。导致代码不符合用户的运行效果嘛? 所以重点就来了。为了用户告诉编译器,请不要为了加快速度,随意调换代码的执行顺序。编译器提供了一些东西给用户去标注,告诉编译器什么什么东西,不能调换。

fn a() {
    X.store(10, Relaxed); // 1
    Y.store(20, Relaxed); // 2
}

看到这里的Relaxed了嘛?这个就是编译器提供了其中一个功能,不仅仅是这个,还有提供其它的,功能不一样的而已。所以用户为了告知编译器怎么怎么操作,用户得学习编译器提供了哪些东西,用户去遵守。 Relaxed这里有两个,X.store(10,Relaxed)的意思是,这行代码上面的所有读写操作代码必须完成,这行代码才能执行。那么编译器看到这个Relaxed之后,它就去看这行代码上面有没有什么读写操作的代码。明显这个是第一行代码,前面根本就没有代码,白搞。 那么来看第二局 Y.store(20, Relaxed); 这里的意思也是一样,设置了Relaxed的意思是,这行代码之前的读写操作的代码必须完成,完成之后才能执行Y.store(20, Relaxed);这行代码,然后Y才变成20.

我们来看看Y.store(20, Relaxed);代码前面有什么代码。有一条,它是X.store(10, Relaxed); // 1 这条代码是写操作,把10写入X。就意味着编译器看到Y.store(20, Relaxed);这条语句后,编译器就不把这条语句调换顺序,把它放在X.store(10, Relaxed);的前面先执行了。这样就实现了保证代码的执行顺序,不会因为编译器觉得可以更快的运行来调换我们代码的执行顺序。

需要注意的是Acquire 和 Release 的排序保证只在线程之内生效,换句话说。内存指令排序的范围是同一个线程。里面的读写操作。如果是两个线程的话,这两个线程里面各自有10条读操作和10条写操作。那么编译器使用内存排序,也仅仅是对单个线程里面的读操作和写操作进行指令重排。不会说一个线程里面的读操作调换到另外一个线程里面去。内存指令排序仅仅是对于同一个线程,这个线程里面有许多的读操作和写操作。编译器会修改同一个线程里面的读写操作之间的顺序。因为编译器觉得这样会加快程序的运行,但是却影响了用户编写多线程同步的功能,导致运行效果和代码的表达不一致问题。

其它的内存指令排序也是同理。 让我们再看如何读这个语句.看这个Acquire是标记在X上面的。这个是编译器提供的。或者标准库就有,提供给用户的。既然提供的,功能就是写好的,这个的功能的意思是,这行代码的下面的代码可能有读和写的操作(我这里给出的一行代码,后面的代码没有给,你就假设这行代码后面有一百行的读写操作,这些操作都是在同一个线程里面的),我使用Acquire标记了X。告诉编译器,后面的代码不要被调换在这行代码的前面,编译器看到就,它就不调换代码,不就达到了我们源代码表达的意思了嘛。

//保证本线程后续的读写操作必须在此原子操作之后执行。
 X.load(Ordering::Acquire)

其它类型的排序也有。下面的给的是cpp的。rust的,不想拷贝过来。 https://rustcc.github.io/Rust_Atomics_and_Locks/3_Memory_Ordering.html 这个文章应该有

std::memory_order_relaxed:不提供任何顺序保证。 std::memory_order_release:保证此原子操作之前的读写操作必须在本原子操作之前执行。 std::memory_order_acq_rel:结合了acquire和release的效果,用于读-修改-写操作。 std::memory_order_seq_cst:提供顺序一致性内存序,是默认的内存序,提供最强的顺序保证。

ggggjlgl 2024-04-18 12:27

我觉得Ordering这东西只有在同时读写的时候才有效果,用来实现避免脏读之类的先后逻辑。但这两个线程中的操作太少,可能某一个线程都执行完了另一个还没提起来,基本上没办法产生数据竞争的场景。

TinusgragLin 2024-04-18 11:57

题主可能误解成,对一原子变量的 acquire load 必须 要在对同一原子变量的 release store 前执行,实际上这不是 acquire 和 release 提供的保证。

--
👇
bestgopher: 这和指令重排有啥关系哦

bestgopher 2024-04-18 11:01

这和指令重排有啥关系哦

ZZG 2024-04-18 10:56

感觉好多回答都复杂了,这里不管什么Ordering都是OK的,Relaxed都行,题主用的Release和Acquire更是没毛病。

问题在于thread1 和 thread2 是新开了两个不同线程,thread2不会等thread1中的内容执行完才开始执行,所以两个线程中内容可能会交替执行,所以thread2可以在thread1赋值true之前先执行load,得到值为false。当然,因为thread1先创立,所以thread1先执行store的概率比较大,所欲输出true的情况更多。如果交换thread1和thread2位置,输出false的情况更多。

如果想稳定输出true,就需要保证thread1一定比thread2先执行完成,方法是把thread1.join() 放在创建thread2前面

TinusgragLin 2024-04-18 09:29

长话短说就是:C++ Memory Order 标准建立了一个基于 happens-before 二元关系的抽象模型,并在这个模型的基础上定义了关于写入可见性的相关保证,这些保证中并没有诸如对于同一原子变量的 acquire load 要在 release store 这样的保证,因此题主看到的不一致是在预料之中的。详细的定义题主可以参照现行 C++ 标准中的 Multi-threaded executions and data racesOrder and consistency 这两节,前者定义了包括 happens-bofere 在内的二元关系和在此基础上的写入可见性保证,后者利用前者定义的二元关系定义了我们熟知的 relaxed、acquire、release、seqcst 这些 memory order 选项。

asuper 2024-04-18 09:26

Ordering是控制指令排序的,不是控制线程间同步的。

指令排序,简单理解就是控制你使用原子操作的这一行,是否允许前面的行跑到他后面去,或者后面的行跑到前面,影响的都是当前线程,和其他线程没有一点关系。

你这里想实现的是线程间同步,可以用mpsc、信号量、或者其他机制来实现

Pikachu 2024-04-18 06:26

打个比方:

我对我的同事说:你干完今天的工作之后,把代码push到Git服务器上去。

这句话隐含的happen-before的关系是:如果我pull的时候看到了他提交的代码,说明他在我pull之前就成功push了他的代码。

但是以下论断都是错的:

我pull的时候一定会看到他push的代码

未必,说不定他今天事情太多,还没干完。

如果我pull的时候没看到他提交的代码,说明他的工作没完成

未必,说不定我pull的时间恰好是他push的时间,服务器有些缓存没刷新。

Pikachu 2024-04-18 05:58

如果线程2中的一个 acquire load 操作 A 读到了线程1中的一个 release store 操作 B 写的值,那么有 B happens-before A.

要注意到,“如果xxx”的内容是B happens-before A的前提条件。只有满足这个前提条件,才能说B happens-before A,但是这个前提条件本身是可以不被满足的。

HC97 2024-04-18 02:21

咱俩说的角度不同,我是在说为什么运行结果不一致,按照我的理解:

对于同线程的两条指令 A 和 B,当 A 求值顺序在 B 之前时,有 A happens-before B.

Acquire 和 Release 就是用来做这个的,它们可以限制对线程内指令的重排序,在线程内制造出确定性的happens-before关系,(也可以说,它保证的是另一个线程可以观测到这个顺序)

结合这三条,就有:如果线程 1 中的一个 acquire load 操作 A 读到了线程2中的一个 release store 操作 B 写的值,那么线程 1 中在 A 之后对 M 的读取就可以看到线程 2 中在 B 之前对 M 的写入。

是这样的,但是线程同步需要在线程间建立确定性的happens-before关系,那要怎么保证操作 A 一定能读到操作 B 写的值呢?只使用 Acquire 和 Release 并不能达到这个目的,还需要手动去判断这个值是否被写入了。

更具体的说,Acquire 和 Release 确实能保证线程 1 观测到线程 2 中的执行顺序,但是它们不能保证线程 1 观测的时机,线程 1 在线程 2 写入前、写入时、写入后 进行观测都有可能。同步线程需要反复观测并根据观测到的值决定是否继续向下执行。

所以题主现在就是缺少了这一步,才导致运行结果不一致。

不过我的理解也不一定对,因为是很久之前看的了,基本上也没怎么用过😢

--
👇
TinusgragLin: > Acquire 和 Release 的排序保证只在线程之内生效

这不对吧,原本引入 Acquire 和 Release 就是为了多线程之间的同步,怎么会只在自己线程内有效呢? 我记得是:

定义一个二元关系 happens-before

  1. 当以下条件满足时,对于内存区域 M 的读取操作 A 可以看见对相同内存区域的写入操作 B:

    • B happens-before A
    • A 与 B 之间没有其他对 M 的写入操作
  2. 如果线程1中的一个 acquire load 操作 A 读到了线程2中的一个 release store 操作 B 写的值,那么有 B happens-before A.

结合这三条,就有:如果线程 1 中的一个 acquire load 操作 A 读到了线程2中的一个 release store 操作 B 写的值,那么线程 1 中在 A 之后对 M 的读取就可以看到线程 2 中在 B 之前对 M 的写入。

--
👇
HC97: 两个线程是独立的,Acquire 和 Release 的排序保证只在线程之内生效

TinusgragLin 2024-04-18 00:57

Acquire 和 Release 的排序保证只在线程之内生效

这不对吧,原本引入 Acquire 和 Release 就是为了多线程之间的同步,怎么会只在自己线程内有效呢? 我记得是:

定义一个二元关系 happens-before

  1. 当以下条件满足时,对于内存区域 M 的读取操作 A 可以看见对相同内存区域的写入操作 B:
    • B happens-before A
    • A 与 B 之间没有其他对 M 的写入操作
  2. 对于同线程的两条指令 A 和 B,当 A 求值顺序在 B 之前时,有 A happens-before B.
  3. 如果线程1中的一个 acquire load 操作 A 读到了线程2中的一个 release store 操作 B 写的值,那么有 B happens-before A.

结合这三条,就有:如果线程 1 中的一个 acquire load 操作 A 读到了线程2中的一个 release store 操作 B 写的值,那么线程 1 中在 A 之后对 M 的读取就可以看到线程 2 中在 B 之前对 M 的写入。

--
👇
HC97: 两个线程是独立的,Acquire 和 Release 的排序保证只在线程之内生效

作者 Rgoogle 2024-04-18 00:42

没有只是看到这部分,好奇像Arc的指针,会不会存在这种问题呢?什么叫排序只在线程内生效。或者说这个指令的排序只是对当前线程有用。对于其它的线程,是不是就不能使用。那么我记得有一个排序好像是会影响其它线程的啊。它是如何实现的呢?

HC97 2024-04-18 00:30

两个线程是独立的,Acquire 和 Release 的排序保证只在线程之内生效

TinusgragLin 2024-04-18 00:22

你是觉得 thread1 应该会在 thread2 之前运行吗?

1 共 16 条评论, 1 页