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等也存在同样的问题?
1
共 16 条评论, 1 页
评论区
写评论首先这不是指令重排导致的, 而是thread1和thread2谁先执行的问题 指令重排只关系到当前线程
至于你想到达“控制多个线程之间的执行顺序”我推荐 Condvar
总结了一下,是这个意思吧,感觉下面的评论也没有回答清楚,因为之前就是因为不知道如何控制,还有以为这个可以控制多个线程之间的执行顺序,实际上根本就控制不了
问题的产生,因为我看到有文章说,编译器和cpu会修改我们的代码,调换顺序,导致代码执行顺序和源代码的执行顺序不一样。 比如以下的代码:
这个代码执行会存在多种输出的情况,其中一种输出特别的奇怪。启动两个独立线程,分别执行a,b函数。 程序执行顺序是 2-> 3 -> 4 -> 1 (这里的数字表示上面源代码注释后面的数字,比如开始是2,说明程序执行的时候,先执行上面注释是2的那行代码) 输出的的结果是 0 20 ,也就是x是0,y是20.
你可能会非常的奇怪。凭什么2这条语句要比1先执行?你的代码明明1是在2的上面,程序按照重上往下执行,凭上面你2先执行呢?难道不应该是先执行1嘛?
这就是重点了。编译器或者cpu会认为这样加快程序的运行效率。所以这么干了。用户很烦恼,难道编译器就不怕这么修改。导致代码不符合用户的运行效果嘛? 所以重点就来了。为了用户告诉编译器,请不要为了加快速度,随意调换代码的执行顺序。编译器提供了一些东西给用户去标注,告诉编译器什么什么东西,不能调换。
看到这里的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。告诉编译器,后面的代码不要被调换在这行代码的前面,编译器看到就,它就不调换代码,不就达到了我们源代码表达的意思了嘛。
其它类型的排序也有。下面的给的是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:提供顺序一致性内存序,是默认的内存序,提供最强的顺序保证。
我觉得Ordering这东西只有在同时读写的时候才有效果,用来实现避免脏读之类的先后逻辑。但这两个线程中的操作太少,可能某一个线程都执行完了另一个还没提起来,基本上没办法产生数据竞争的场景。
题主可能误解成,对一原子变量的 acquire load 必须 要在对同一原子变量的 release store 前执行,实际上这不是 acquire 和 release 提供的保证。
--
👇
bestgopher: 这和指令重排有啥关系哦
这和指令重排有啥关系哦
感觉好多回答都复杂了,这里不管什么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前面
长话短说就是:C++ Memory Order 标准建立了一个基于
happens-before
二元关系的抽象模型,并在这个模型的基础上定义了关于写入可见性的相关保证,这些保证中并没有诸如对于同一原子变量的 acquire load 要在 release store 这样的保证,因此题主看到的不一致是在预料之中的。详细的定义题主可以参照现行 C++ 标准中的 Multi-threaded executions and data races 和 Order and consistency 这两节,前者定义了包括happens-bofere
在内的二元关系和在此基础上的写入可见性保证,后者利用前者定义的二元关系定义了我们熟知的 relaxed、acquire、release、seqcst 这些 memory order 选项。Ordering是控制指令排序的,不是控制线程间同步的。
指令排序,简单理解就是控制你使用原子操作的这一行,是否允许前面的行跑到他后面去,或者后面的行跑到前面,影响的都是当前线程,和其他线程没有一点关系。
你这里想实现的是线程间同步,可以用mpsc、信号量、或者其他机制来实现
打个比方:
这句话隐含的happen-before的关系是:如果我pull的时候看到了他提交的代码,说明他在我pull之前就成功push了他的代码。
但是以下论断都是错的:
未必,说不定他今天事情太多,还没干完。
未必,说不定我pull的时间恰好是他push的时间,服务器有些缓存没刷新。
要注意到,“如果xxx”的内容是B happens-before A的前提条件。只有满足这个前提条件,才能说B happens-before A,但是这个前提条件本身是可以不被满足的。
咱俩说的角度不同,我是在说为什么运行结果不一致,按照我的理解:
Acquire 和 Release 就是用来做这个的,它们可以限制对线程内指令的重排序,在线程内制造出确定性的
happens-before
关系,(也可以说,它保证的是另一个线程可以观测到这个顺序)是这样的,但是线程同步需要在线程间建立确定性的
happens-before
关系,那要怎么保证操作 A 一定能读到操作 B 写的值呢?只使用 Acquire 和 Release 并不能达到这个目的,还需要手动去判断这个值是否被写入了。更具体的说,Acquire 和 Release 确实能保证线程 1 观测到线程 2 中的执行顺序,但是它们不能保证线程 1 观测的时机,线程 1 在线程 2 写入前、写入时、写入后 进行观测都有可能。同步线程需要反复观测并根据观测到的值决定是否继续向下执行。
所以题主现在就是缺少了这一步,才导致运行结果不一致。
不过我的理解也不一定对,因为是很久之前看的了,基本上也没怎么用过😢
--
👇
TinusgragLin: > Acquire 和 Release 的排序保证只在线程之内生效
这不对吧,原本引入 Acquire 和 Release 就是为了多线程之间的同步,怎么会只在自己线程内有效呢? 我记得是:
定义一个二元关系
happens-before
,当以下条件满足时,对于内存区域 M 的读取操作 A 可以看见对相同内存区域的写入操作 B:
happens-before
A如果线程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 的排序保证只在线程之内生效
这不对吧,原本引入 Acquire 和 Release 就是为了多线程之间的同步,怎么会只在自己线程内有效呢? 我记得是:
定义一个二元关系
happens-before
,happens-before
Ahappens-before
B.happens-before
A.结合这三条,就有:如果线程 1 中的一个 acquire load 操作 A 读到了线程2中的一个 release store 操作 B 写的值,那么线程 1 中在 A 之后对 M 的读取就可以看到线程 2 中在 B 之前对 M 的写入。
--
👇
HC97: 两个线程是独立的,Acquire 和 Release 的排序保证只在线程之内生效
没有只是看到这部分,好奇像Arc的指针,会不会存在这种问题呢?什么叫排序只在线程内生效。或者说这个指令的排序只是对当前线程有用。对于其它的线程,是不是就不能使用。那么我记得有一个排序好像是会影响其它线程的啊。它是如何实现的呢?
两个线程是独立的,Acquire 和 Release 的排序保证只在线程之内生效
你是觉得 thread1 应该会在 thread2 之前运行吗?