Skip to content
On this page

最近rust 日报公众号发了一篇文章,有一个哥们做了rust atomic的测试,主要是帮助大家理解atomic的工作机制. 我顺道来解释一下atomic是怎么工作的以及为什么会是这样.

atomic 介绍

atomic的话题比较复杂, 涉及的内容较多,说一下,我的理解,主要从两个方面.

  1. 操作的原子性,也就是CAS,主要解决a+=1这种操作的原子性问题.
  2. 执行顺序问题,这个才是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

这个不仅保证了正确性,同时也是性能最优.