Appearance
最近rust 日报公众号发了一篇文章,有一个哥们做了rust atomic的测试,主要是帮助大家理解atomic的工作机制. 我顺道来解释一下atomic是怎么工作的以及为什么会是这样.
atomic 介绍
atomic的话题比较复杂, 涉及的内容较多,说一下,我的理解,主要从两个方面.
- 操作的原子性,也就是CAS,主要解决a+=1这种操作的原子性问题.
- 执行顺序问题,这个才是atomic比较难理解的地方.
atomic的执行顺序
Order主要有五种模式:
- Relaxed load,store都能用
- Release 仅能用于store
- Acquire 仅能用于load
- AcqRel load,store都能用,实际上可以认为是Release和Acquire的合体,有点类似于语法糖.
- SeqCst load,store都能用
如何理解这五种模式呢?
Relaxed模式
实现了CAS功能,也就是只保证原子的操作性,不保证执行顺序.
Acquire,Release,AcqRel
如何理解执行顺序呢? 这个的关键地方是性能,为了让程序更快的执行,我们的编译器和CPU可以做很多优化. 比如优化指令的执行顺序,这不仅涉及到编译器,也涉及到CPU,都可能会对执行令进行重排. 但是有时候我们想阻止这种重排行为,该怎么告诉编译器和cpu呢?
这就是atomic的Order.
Release 告诉cpu不要把我这条指令前面的指令放到我后面执行,而Acquire则是告诉CPU,不要把我后面的执行放到我前面执行.
这个很难记,但是其实很容易理解,我们对比lock来理解,lock主要用来保护一个临界区域,主要是保护从lock到unlock的一段指令的执行. 而lock类似于Acquire,unlock类似于Release,就是为了保证这段指令的执行.
SeqCst
指令前的不能挪到后面执行,指令后面的不能挪到前面执行. 所以如果你搞不清楚的时候就用这个,但是它的问题也很明显:
因为顺序不能自由调整,性能上不如Release/Acquire配合
代码的理解
这里把代码粘贴在这里,方便大家阅读:
rust
#[cfg(test)]
mod tests {
use std::sync::atomic::Ordering::{Acquire, Relaxed, Release, SeqCst};
use std::sync::atomic::Ordering;
use loom::{
sync::{atomic::AtomicUsize, Arc},
thread,
};
fn two_numbers_with_a_dependency(
read_ordering_a: Ordering,
read_ordering_b: Ordering,
write_ordering_a: Ordering,
write_ordering_b: Ordering,
) {
loom::model(move || {
let num_a = Arc::new(AtomicUsize::new(1));
let num_b = Arc::new(AtomicUsize::new(0));
let num_a2 = num_a.clone();
let num_b2 = num_b.clone();
let tb = thread::spawn(move || {
for idx in 1..4 {
num_a2.store(idx + 1, read_ordering_a);
num_b2.store(idx, read_ordering_b);
}
});
let _ = thread::spawn(move || {
for _ in 1..4 {
let nb = num_b.load(write_ordering_b);
let na = num_a.load(write_ordering_a);
assert!(na >= nb);
}
});
tb.join().unwrap();
});
}
#[test]
#[should_panic]
fn atomics_relaxed_failing() {
two_numbers_with_a_dependency(Relaxed, Relaxed, Relaxed, Relaxed);
}
#[test]
#[should_panic]
fn atomics_writer_release_reader_relaxed_failing() {
two_numbers_with_a_dependency(Release, Release, Relaxed, Relaxed);
}
#[test]
fn atomics_writer_release_reader_seqcst_and_relaxed() {
two_numbers_with_a_dependency(Release, Release, Relaxed, SeqCst);
}
#[test]
fn atomics_seq_cst_does_not_fail() {
two_numbers_with_a_dependency(SeqCst, SeqCst, SeqCst, SeqCst);
}
#[test]
fn atomics_acquire_release_does_not_fail() {
two_numbers_with_a_dependency(Release, Release, Acquire, Acquire);
}
}
顺便说一下loom,这个一个帮助并发测试的工具,方便发现潜在的竞争冲突问题.
atomics_relaxed_failing
这个失败,很自然,他不能保证顺序的执行,所以不能保证读到数据之间的逻辑性.
two_numbers_with_a_dependency
这个失败的原因是,保证store的顺序,但是load的顺序不能保证.
atomics_writer_release_reader_seqcst_and_relaxed
这个可以成功的原因是write_ordering_b
是seqCst,他保证了load的顺序,而Release保证了store的顺序.
atomics_seq_cst_does_not_fail
这个是最严苛的模式,所以性能也是最低的.
atomics_acquire_release_does_not_fail
这个不仅保证了正确性,同时也是性能最优.