专栏名称: 程序人生
十年漫漫程序人生,打过各种杂,也做过让我骄傲的软件;管理过数十人的团队,还带领一班兄弟姐妹创过业,目前在硅谷一家创业公司担任 VP。关注程序人生,了解程序猿,学做程序猿,做好程序猿,让我们的程序人生精彩满满。
目录
相关文章推荐
51好读  ›  专栏  ›  程序人生

透过 Rust 探索系统的本原:并发原语

程序人生  · 公众号  · 程序员  · 2021-04-05 08:05

正文

请到「今天看啥」查看全文


pub enum Ordering {    Relaxed,    Release,    Acquire,    AcqRel,    SeqCst,}

文档里解释了几种 Ordering 的用途,我来稍稍扩展一下:

  1. Relaxed:这是最宽松的规则,它对编译器和 CPU 不做任何限制,可以乱序

  1. Release:当我们写入数据(上面的 store)的时候,如果用了 Release order,那么:

  • 对于其它线程,如果它们使用了 Acquire 来读取这个 atomic 的数据, 那么它们看到的是修改后的结果。因为上文中我们在 compare_exchange 里使用了 Acquire 来读取,所以保证读到最新的值。

  • 对于当前线程,任何读取或写入操作都不能被被乱序排在这个 store 之后。也就是说,在上文的例子里,CPU 或者编译器不能把 **3 挪到 **4 之后执行。

  1. Acquire:当我们读取数据的时候,如果用了 Acquire order,那么:

  • 对于其它线程,如果使用了 Release 来修改数据,那么,修改的值对当前线程可见。

  • 对于当前线程,任何读取或者写入操作都不能被乱序排在这个读取之前。在上文的例子里,CPU 或者编译器不能把 **3 挪到 **1 之前执行。

  1. AcqRel:Acquire 和 Release 的结合,同时拥有 Acquire 和 Release 的保证。这个一般用在 fetch_xxx 上,比如你要对一个 atomic 自增 1,你希望这个操作之前和之后的读取或写入操作不会被乱序,并且操作的结果对其它线程可见。

  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},};
// server statisticspub struct Metrics(HashMapstatic str, AtomicUsize>);
impl Metrics { pub fn new(names: &[&'static






请到「今天看啥」查看全文


推荐文章
健康常识百科  ·  春节过后,人人都需要吃的是这一碗!
8 年前
朱莉生活日记  ·  春天这样养生,健康一整年!
8 年前
点点星光  ·  我 的 照 片!
8 年前