Table of Contents generated with DocToc
假设要实现一个计数功能,每次对变量执行+1的操作。CPU执行的时候就需要顺序执行三个操作
- 从内存中读取变量的值
- 加1
- 写回到内存中
如果有两个线程同时操作了这个值,这三个操作可能是交叉的,导致结果不正确
fn f(a: &mut i32, b: &mut i32) {
*a += 1;
*b += 1;
*a += 1;
}
交给操作系统编译执行,但很可能你得到的是这样的
fn f(a: &mut i32, b: &mut i32) {
*a += 2;
*b += 1;
}
只要不影响程序语义,指令可以重排执行以优化,即不按代码顺序执行。
单线程下这样问题可能还不大,但如果多线程下,同一线程下多条原子指令,也是会有指令重排的可能,数据竞争很有可能发生,就是说加了原子操作也无法确定数据操作顺序
。
Rust用于的内存访问顺序(memory order)的Ordering基本和C++ 20
的内存排序的保持一致
#[stable(feature = "rust1", since = "1.0.0")]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
#[non_exhaustive]
#[rustc_diagnostic_item = "Ordering"]
pub enum Ordering {
Relaxed,
Release,
Acquire,
AcqRel,
SeqCst,
}
- Relaxed,这是最宽松的规则,它对编译器和 CPU 不做任何限制,可以乱序执行。
- Release,适用于写数据操作
- 当前线程不能有其他的读或写被 reorder 在 store 之后当前写入后的结果对其他线程的同一数据 Acquire 读取操作是可见的
- Acquire,适用于读取数据操作
- 当前线程不能有其他的读或写被 reorder 在 load 之前其他线程的同一数据已发生的 Release 写入操作都是对其可见的
- AcqRel ,是 Acquire 和 Release 的结合
- 一般用在 fetch_xxx 上,比如你要对一个 atomic 自增 1,你希望这个操作之前和之后的读取或写入操作不会被乱序,并且操作的结果对其它线程可见。
- SeqCst,除了 AcqRel 的保证外,它还保证所有线程看到的所有 SeqCst 操作的顺序是一致的。