正文
pub enum Ordering {
Relaxed,
Release,
Acquire,
AcqRel,
SeqCst,
}
文档里解释了几种 Ordering 的用途,我来稍稍扩展一下:
-
Relaxed:这是最宽松的规则,它对编译器和 CPU 不做任何限制,可以乱序
-
Release:当我们写入数据(上面的 store)的时候,如果用了
Release
order,那么:
-
Acquire:当我们读取数据的时候,如果用了
Acquire
order,那么:
-
AcqRel:Acquire 和 Release 的结合,同时拥有 Acquire 和 Release 的保证。这个一般用在
fetch_xxx
上,比如你要对一个 atomic 自增 1,你希望这个操作之前和之后的读取或写入操作不会被乱序,并且操作的结果对其它线程可见。
-
SeqCst:最严格的 ordering,除了
AcqRel
的保证外,它还保证所有线程看到的所有的
SeqCst
操作的顺序是一致的。
因为 CAS 和 ordering 都是系统级的操作,所以上面我描述的 Ordering 的用途在各种语言中都大同小异。对于 Rust 来说,它的 atomic 原语是继承于 C++,见[5]。如果读 Rust 的文档你感觉云里雾里,那么 C++ 的关于 ordering 的文档要清晰得多。
好,上述的锁的实现的完整代码如下:
pub fn with_lock(&self, op: impl FnOnce(&mut T) -> R) -> R {
while self
.locked
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
while self.locked.load(Ordering::Relaxed) == true {}
}
let ret = op(unsafe { &mut *self.v.get() });
self.locked.store(false, Ordering::Release);
ret
}
注意,我们在 while loop 里,又嵌入了一个 loop,这是因为 CAS 是个代价比较高的操作,它需要获得对应内存的独占访问(exclusive access),我们希望失败的时候只是简单读取 atomic 的状态,只有符合条件的时候再去做独占访问,进行 CAS。所以,看上去我们多做了一层循环,实际代码的效率更高。
以下是两个线程同步的过程,一开始 t1 拿到锁,t2 spin,之后 t1 释放锁,t2 进入到临界区执行:
通过上面的例子,相信你对 atomic 以及其背后的 CAS 有个初步的了解,如果你还想对 Rust 下使用 atomic 有更多更深入的了解,可以看 Jon Gjengset 最新一期 Crust of Rust: Atomics and Memory Ordering [6]。巧的是这周我计划写有关并发原语的文章,Jon 的视频就出来了,帮我进一步夯实了关于 atomic 的知识。
上文中,为了展示如何使用 atomic,我们制作了一个非常粗糙简单的 SpinLock [7]。SpinLock,顾名思义,就是线程通过 CPU 空转(spin,就像上文中的 while loop),来等待某个锁可用的一种锁。SpinLock 和 Mutex lock 最大的不同是,使用 SpinLock,线程在忙等(busy wait),而使用 Mutex lock,线程会在等待锁的时候被调度出去,等锁可用时再被被调度回来。
听上去 SpinLock 似乎效率很低,但这要具体看锁的临界区的大小。如果临界区要执行的代码很少,那么和 Mutex lock 带来的上下文切换(context switch)相比,SpinLock 是值得的。在 Linux Kernel 中,很多时候,我们只能使用 SpinLock。
Rust 的 spin-rs crate [8] 提供了 spinlock 的实现。
那么,atomic 除了做其它并发原语,还有什么作用?
我个人用的最多的是做各种 lock-free 的数据结构。比如,我们需要一个全局的 id 生成器。我们当然可以使用 uuid 这样的模块来生成唯一的 id,但如果我们同时需要这个 id 是有序的,那么
AtomicUsize
就是最好的选择。你可以用
fetch_add
来增加这个 id,而
fetch_add
返回的结果就可以用于当前的 id。这样,我们不需要加锁,就得到了一个可以在多线程中安全使用的 id 生成器。
另外,atomic 还可以用于记录系统的各种 metrics。比如我做的一个简单的 in-memory Metrics 模块:
use std::{
collections::HashMap,
sync::atomic::{AtomicUsize, Ordering},
};
pub struct Metrics(HashMapstatic str, AtomicUsize>);
impl Metrics {
pub fn new(names: &[&'static