原子类型和原子操作
原子(atom)指的是一系列不可被 CPU 上下文交流的机器指令,这些指令组合在一起就形成了原子操作。在多核 CPU 下,当某个 CPU 中心开始运转原子操作时,会先暂停其它 CPU 内核对内存的操作,以确保原子操作不会被其它 CPU 内核所搅扰。
原子操作( atomic operation )是指不可分割且不可中止的一个或一系列操作,在并发编程中需求由 CPU 层面做出一些确保,让一系列操作成为原子操作。 一个原子操作从开始到结束能够是一个操作步骤,也能够包括多个操作步骤,这些步骤的顺序不能够被打乱,履行进程也不会被其他机制打断。
注:由于原子操作是经过指令供给的支撑,因而它的功用比较锁和消息传递会好许多。比较较于锁而言,原子类型不需求开发者处理加锁和开释锁等问题,一起支撑修正,读取等操作,并具有较高的并发功用,简直一切的言语都支撑原子类型。
原子类型是用来协助开发者更轻松的完成原子操作的数据类型,原子类型是无锁类型,可是无锁不代表无需等候,由于原子类型内部运用了CAS
循环,当大量的冲突发生时,该等候仍是得等候!可是总之比锁要好。
注:CAS 全称是 Compare and swap, 它经过一条指令读取指定的内存地址,然后判别其中的值是否等于给定的前置值,假如持平,则将其修正为新的值
Atomic 原子操作作为一个并发原语,是完成一切并发原语的柱石,简直一切的言语都支撑原子类型和原子操作,比方 Java 的java.util.concurrent.atomic
供给了许多原子类型,Go 言语的sync/atomic
包供给了对原子操作的支撑,Rust 也不破例。
注:原子操作是 CPU 的概念,而编程言语中也有相似的概念,叫做并发原语。并发原语是内核供给给外核调用的函数,这种函数在履行进程中不允许中止。
Rust 中的 Atomic 并发原语
Rust 中的原子类型位于std::sync::atomic module
中。
这个 module 的文档中对原子类型有如下描绘: Rust 中的原子类型在线程之间供给原始的同享内存通讯,而且是其他并发类型的构建基础。
std::sync::atomic module
现在共供给了以下12种原子类型:
AtomicBool
AtomicI8
AtomicI16
AtomicI32
AtomicI64
AtomicIsize
AtomicPtr
AtomicU8
AtomicU16
AtomicU32
AtomicU64
AtomicUsize
原子类型与一般的类型基本上没有太多的区别,例如AtomicBool
和bool
,仅仅一个能够在多线程中运用,另一个则更适用于单线程下运用。
以AtomicI32
为例,它的定义是一个结构体,有以下原子操作相关的办法:
pub fn fetch_add(&self, val: i32, order: Ordering) -> i32 - 对原子类型进行加(或减)运算
pub fn compare_and_swap(&self, current: i32, new: i32, order: Ordering) -> i32 - CAS(rust 1.50废弃, 由compare_exchange代替)
pub fn compare_exchange(&self, current: i32, new: i32, success: Ordering, failure: Ordering) -> Result<i32, i32> - CAS
pub fn load(&self, order: Ordering) -> i32 - 从原子类型内部读取值
pub fn store(&self, val: i32, order: Ordering) - 向原子类型内部写入值
pub fn swap(&self, val: i32, order: Ordering) -> i32 - 交流
能够看到每个办法都有一个 Ordering 类型的参数,Ordering
是一个枚举,表明该操作的内存屏障的强度,用于控制原子操作运用的内存顺序。
注:内存顺序是指 CPU 在拜访内存时的顺序,该顺序或许受以下因素的影响:
- 代码中的先后顺序
- 编译器优化导致在编译阶段发生改动(内存重排序 reordering)
- 运转阶段因 CPU 的缓存机制导致顺序被打乱
pub enum Ordering {
Relaxed,
Release,
Acquire,
AcqRel,
SeqCst,
}
Rust 中 Ordering 这个枚举的枚举值别离代表什么:
- Relaxed, 这是最宽松的规则,它对编译器和 CPU 不做任何限制,能够乱序
- Release 开释,设定内存屏障(Memory barrier),确保它之前的操作永远在它之前,可是它后面的操作或许被重排到它前面(用于写入)
-
Acquire 获取,设定内存屏障,确保在它之后的拜访永远在它之后,可是它之前的操作却有或许被重排到它后面,往往和
Release
在不同线程中联合运用(用于读取) -
AcqRel, 是Acquire和Release的结合,一起拥有它们俩供给的确保。关于
load
,它运用的是 Acquire 指令,关于store
,它运用的是 Release 指令,期望该操作之前和之后的读取或写入操作不会被从头排序。AcqRel一般用在fetch_add
上 -
SeqCst 顺序一致性,
SeqCst
就像是AcqRel
的加强版,它不论原子操作是属于读取仍是写入的操作,只需某个线程有用到SeqCst
的原子操作,线程中该SeqCst
操作前的数据操作绝对不会被从头排在该SeqCst
操作之后,且该SeqCst
操作后的数据操作也绝对不会被从头排在SeqCst
操作前;它还确保一切线程看到的一切的SeqCst
操作的顺序是一致的(尽管功用低,可是最保险)
经过这个Ordering
枚举类型的参数,开发者能够自己定制底层的 Memory Ordering。
注:什么是 Memory Ordering, 摘录维基百科中的定义:
Memory Ordering (内存排序) 是指 CPU 拜访主存时的顺序。能够是编译器在编译时发生,也能够是 CPU 在运转时发生。反映了内存操作重排序,乱序履行,然后充分利用不同内存的总线带宽。现代处理器大都是乱序履行。因而需求内存屏障以确保多线程的同步。
关于对Memory Ordering 的了解,有两个线程都要操作 AtomicI32 类型,假定 AtomicI32 类型数据初始值是0,一个线程履行读操作,另一个线程履行写操作要将数据写为10。假定写操作履行完成后,读线程再履行读操作就一定能读到数据10吗? 答案是不确定的,由于不同编译器的完成和CPU的优化策略,或许会出现尽管写线程履行完写操作了,但最新的数据还存在CPU的寄存器中,还没有同步到内存中。为了确保寄存器到内存中的数据同步,就需求Memory Ordering了。 Release 能够了解为将寄存器的值同步到内存,Acquire 是疏忽当前寄存器中存的值,而直接去内存中读取最新的值。 例如当咱们调用原子类型的 store 办法时供给的 Ordering 是 release,在调用原子类型的load 办法时供给的 Ordering 是 Acquire 就能够确保履行读操作的线程一定会读到寄存器里最新的值。
多线程中运用 Atomic
由于原子类型都完成了Sync trait
,所以原子类型的变量在线程之间同享是安全的,但由于它们自身没有供给同享机制,因而比较常见的用法是将其放在原子引证计数智能指针Arc
中。 下面是官方文档中一个简略的自旋锁的例子:
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
fn main() {
// 运用原子类型创建一个锁,经过引证计数取得同享一切权
let spinlock = Arc::new(AtomicUsize::new(1));
// 引证计数 +1
let spinlock_clone = spinlock.clone();
let thread = thread::spawn(move || {
// SeqCst排序:写操作(存储)运用release 语义:写屏障之前的读写操作不能重排在写屏障之后
spinlock_clone.store(0, Ordering::SeqCst);
});
// 运用 while循环,来等候某个临界区可用的一种锁
// SeqCst排序:读操作(读取)运用 acquire 语义 读屏障之后的读写操作不能重排到读写屏障之前
// 上面的线程中的写(存储)指令,下面的指令要求之后的读写操作不能在此之前
while spinlock.load(Ordering::SeqCst) != 0 {}
if let Err(panic) = thread.join() {
println!("Thread had an error: {:?}", panic);
}
}
注:自旋锁是指当一个线程测验去获取某一把锁的时候,假如这个锁此刻现已被其他线程获取,那么此线程就无法获取到这把锁,该线程将会等候,间隔一段时刻后会再次测验获取。 自旋锁实际上是经过 CPU 空转 (spin) 忙等候 (budy wait),例如上面代码中的 while 循环,来等候某个临界区可用的一种锁。
运用自旋锁能够的削减线程的阻塞,适用于对锁的竞赛不剧烈,且占用锁时刻十分短的场景。 可是假如锁的竞赛剧烈,或许持有锁的线程需求长时刻占用锁,受维护的临界区过大,线程自旋的就耗费大于线程阻塞挂起操作的耗费,自旋操作会一向占用CPU做无用功,就会形成CPU浪费,其他需求CPU的线程反而不能取得CPU,体系功用会急剧下降。
上面例子是自旋锁功用的完成,而且运用的内存排序是Ordering::SeqCst
,下面咱们测验完成一个自选锁:
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::thread;
use std::time::Duration;
struct SpinLock {
lock: AtomicBool,
}
impl SpinLock {
pub fn new() -> Self {
Self {
lock: AtomicBool::new(false),
}
}
pub fn lock(&self) {
while self
.lock
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
// 测验加锁, 假如加锁失利则一向自旋
{
// CAS的耗费比较大, 当加锁失利时, 经过简略load读取锁的状况, 只需读取到锁被开释时才会再去测验CAS加锁
while self.lock.load(Ordering::Relaxed) {}
}
}
pub fn unlock(&self) {
// 解锁
self.lock.store(false, Ordering::Release);
}
}
fn main() {
let spinlock = Arc::new(SpinLock::new());
let spinlock1 = spinlock.clone();
let thread = thread::spawn(move || {
// 子线程加锁1,内部调用了compare_exchange 办法,修正状况
spinlock1.lock();
thread::sleep(Duration::from_millis(100));
println!("do something1!");
// 子线程解锁1
spinlock1.unlock();
});
thread.join().unwrap();
// 主线程加锁
spinlock.lock();
println!("do something2!");
// 主线程解锁
spinlock.unlock();
}
上面咱们完成的自旋锁,本质便是一个原子类型AtomicBool
,它的初始值为false
。
当履行lock
办法进行加锁操作时,利用了原子操作CAS
的特性,假如compare_exchange
失利,则测验加锁的线程会卡在这个while
循环中自旋。 这里有一个功用上的小优化,由于履行CAS
耗费代价比较大,所以在CAS
失利时,再不断经过简略load
读取锁的状况, 只有读取到锁被开释时才会再去测验CAS
加锁。这样效率更好一些。
当履行unlock
办法时,直接将AtomicBool
设置store
为false
,选用的 Memory Ordering 是Release,会将寄存器中的值与内存中的值同步,内存中就为false
。此刻,假如有线程卡在lock
办法while
循环处自旋,CAS
操作compare_exchange
选用的 Memory Ordering 是Acquire
,将会疏忽其自己当前寄存器中的值,从内存中读取到新的值为false
,CAS
将履行成功,也便是加锁成功。
Atomic 能代替锁吗
那么原子类型既然这么万能,它能够代替锁吗?答案是不可:
- 关于复杂的场景下,锁的运用简略粗暴,不容易有坑;
-
std::sync::atomic
包中仅供给了数值类型的原子操作:AtomicBool
,AtomicIsize
,AtomicUsize
等,而锁能够使用于各种类型; - 在有些情况下,必须运用锁来合作,比方
Mutex
,RwLock
,Condvar
等;
Atomic 的使用场景
事实上,Atomic
尽管关于用户不太常用,可是关于高功用库的开发者、标准库开发者都十分常用,它是并发原语的柱石,除此之外,还有一些场景适用:
- 无锁(lock free)数据结构
- 全局变量,例如全局自增 ID, 在后续章节会介绍
- 跨线程计数器,例如能够用于统计指标
以上列出的仅仅Atomic
适用的部分场景,具体场景需求我们未来根据自己的需求进行权衡选择。
总结
原子(atom)便是类比生物学中不可再分的原子,原子操作(atomic operation)便是“不可再被中止的一个或一系列操作”。原子类型是用来协助开发者更轻松的完成原子操作的数据类型。并发原语是内核供给给外核调用的函数,这种函数在履行进程中不允许中止。
Atomic
原子类型是无锁类型,内部运用了CAS
循环,不需求开发者处理加锁和开释锁的问题,一起支撑修正,读取等原子操作,这些操作是经过指令供给的支撑,因而它的功用比较锁和消息传递会好许多。原子操作需求合作运用Ordering
内存排序,经过这个Ordering
枚举类型的参数,开发者能够自己定制底层的 Memory Ordering。由于Atomic
原子类型功用比锁高不少,所以在 Rust 中有广泛的运用场景,比方作为作为全局变量,作为跨线程变量等,可是无法完全替代锁,由于锁足够简略。
原子操作能够概括为以下5类操作:
-
fetch_add
– 对原子类型进行加(或减)运算 -
compare_and_swap
和compare_exchange
– 比较,假如持平则进行交流 -
load
– 从原子类型内部读取值 -
store
– 向原子类型内部写入值 -
swap
– 交流
参考
- course.rs/advance/con…
- blog.frognew.com/2020/07/rus…
- rustmagazine.github.io/rust_magazi…
- rustcc.cn/article?id=…
- doc.rust-lang.org/std/sync/at…
- doc.rust-lang.org/nomicon/ato…
- zhuanlan.zhihu.com/p/365905573