Rust in Everything
基于 parking_lot 0.12.5

parking_lot 解决什么问题

主线位置:四条主线的出发点。本页回答「为什么要有一个全局停车场」,主线 1、2 从下一页开始。
flowchart LR
  subgraph lockside["锁本身:几个状态位"]
    m1["Mutex A:1 字节"]
    m2["Mutex B:1 字节"]
    cv["Condvar C:1 个指针"]
  end
  subgraph lot["全局停车场:一张哈希表"]
    b0["桶 0:WordLock + 链表"]
    b1["桶 1:WordLock + 链表"]
    bn["桶 n:……"]
  end
  m1 -->|"地址 hash"| b1
  m2 -->|"地址 hash"| b0
  cv -->|"地址 hash"| b1
  b1 --> t1["线程 X(key = A)"]
  t1 --> t2["线程 Y(key = C)"]

停车场的想法

一把能让线程睡下的锁,至少要两样东西:一个表示「锁着没有」的状态,和一条「谁在等」的队列。最直接的做法是两样都放在锁里,pthread mutex、Windows 的 critical section 都是这样,锁因此有几十字节。parking_lot_core 的 crate 文档说它的做法是把后一样拿出去(core/src/lib.rs:8-28):

To keep synchronization primitives small, all thread queuing and suspending functionality is offloaded to the parking lot. The idea behind this is based on the Webkit WTF::ParkingLot class, which essentially consists of a hash table mapping of lock addresses to queues of parked (sleeping) threads. The Webkit parking lot was itself inspired by Linux futexes, but it is more powerful since it allows invoking callbacks while holding a queue lock.

拆成三点:

  1. 锁只剩状态位。 RawMutex 是一个 AtomicU8,只用最低两位:LOCKED_BIT 和 PARKED_BIT(「有人在等」)。无竞争时加锁、解锁各一次 CAS,从头到尾不碰停车场。
  2. 等待队列按地址找。 全进程只有一张哈希表,桶里挂着正在睡的线程。线程要等某把锁时,用锁的地址当 key 找到桶,把自己挂上去;解锁方用同一个地址找到同一个桶,摘一个线程叫醒。不同的锁可能落进同一个桶,桶里的链表按 key 区分。表的大小随线程数增长,而不是随锁的数量增长:锁再多,同一时刻在睡的线程也不会超过线程总数。
  3. 在队列锁里跑回调。 这是和 futex 的区别。futex 的「检查条件再睡」只能比较一个整数;停车场让调用方传一个闭包 validate,在桶锁里执行,返回 false 就不睡。解锁一侧同样有一个闭包 callback,在桶锁里决定这次是直接把锁交给被叫醒者,还是放开锁。Condvar 的 requeue、RwLock 一次叫醒一批读者,都是靠这些回调做到的。代价是一套 unsafe 契约:回调不能 panic、不能再调停车场,key 必须是「你控制的地址」(park 与四种 unpark)。

README 第 13 条的 eventual fairness 链接的也是 WebKit 的一个 changeset:每个桶有一个计时器,平均每 0.5 ms 强制一次公平交接(eventual fairness)。本卷只交代这层渊源,不展开 WebKit 的实现。

和 std 的分工

两边都提供 Mutex、RwLock、Condvar、Once,接口相似,取舍不同。下表的 std 一列是 rustc 1.99.1 的 std,大小是本机(macOS aarch64)实测(E1,和 std Mutex 比):

parking_lot 0.12.5std(1.99.1)
大小Mutex<()> 1 字节,RwLock<()> 8,Condvar 8,Once 1本机 Mutex<()> 16,RwLock<()> 16,Condvar 16,Once 8
毒化没有。持锁的线程 panic 后,别的线程照常加锁有。lock() 返回 LockResult,持锁 panic 后返回 PoisonError
公平默认允许插队,每个桶约每 0.5 ms 强制一次交接;另有 unlock_fair、FairMutex、bump没有公平相关的 API;行为取决于平台实现
Condvar承诺无虚假唤醒;notify_all 只叫醒一个、其余 requeue;同一时刻只能配一把 mutex,没有等待者时可以换文档说可能虚假唤醒;macOS 的 pthread 实现在第一次 wait 时记下 mutex 地址、此后不清除,换一把 mutex 就 panic(condvar/pthread.rs:32-41)
可定制lock_api 外壳 + parking_lot_core 停车场,可以写自己的锁;有可升级读、降级、Arc 守卫、死锁检测不可定制
平台实现自己的停车场;线程最终睡在每线程一个的 parker 上:macOS 是 pthread 条件变量,Linux 是 futexLinux 等平台用 futex(sys/sync/mutex/mod.rs:1-16);macOS 落到 target_family = "unix" 分支,用 pthread mutex,懒分配在 OnceBox 里(mod.rs:21-27、pthread.rs:8-28)

最后一行决定了「本机对比」的含义:在这台 Mac 上比较 parking_lot 和 std,比的是「停车场 + 每线程 pthread 条件变量」对「每把锁一个堆上的 pthread mutex」;在 Linux 上比的是两个都基于 futex 的实现。两者的结论不能互相搬。std 在 macOS 上为什么不用更轻的原语,ThreadParker 的各平台实现 里引了 std 自己的注释。

三层 crate

crate回答的问题本卷
parking_lot_core线程怎样按地址睡下、被叫醒,而不丢唤醒02
lock_api给定一把「原始锁」,怎样包出类型安全的 Mutex<T> / RwLock<T> 和守卫06
parking_lot用前两层实现具体的锁:状态位怎么设计、何时停车、何时交接03–05

lock_api 不依赖 core,所以别的 crate 可以只用外壳(spin 的 lock_api feature 就是这样),也可以只用停车场写自己的原语(11 动手)。三层的文件清单见 源码地图。

谁在用

本站前几卷里已经出现过 parking_lot:

在哪用法那一卷
tokio 的 parking_lot feature(full 包含它)「不加 API、只换实现」:内部的 Mutex / RwLock / Condvar 换成 parking_lot 的tokio:crate 图
dioxus SyncStorage每个格子是一个 parking_lot::RwLock;同一线程拿着写锁再读会卡死dioxus:generational-box
probe-rs 的 GDB 服务端Session 包在 parking_lot::FairMutex 里probe-rs:GDB 服务端
tracing-subscriber 的 parking_lot featurereload 层的 RwLock 换成 parking_lot 的tracing:reload
burn 的反向传播同一线程重入自己那张图的锁会自锁burn:反向传播与梯度
sqlx(经 futures-intrusive)、pingora间接依赖10 在本站中

其中 dioxus 和 burn 的两个现象都是「parking_lot 的锁不可重入」,09 常见坑 会从源码解释。

「比 std 快」从哪来

README 第 19–23 行:

When tested on x86_64 Linux, parking_lot::Mutex was found to be 1.5x faster than std::sync::Mutex when uncontended, and up to 5x faster when contended from multiple threads.

这句话有三个限定:x86_64、Linux、当时的 std。三者现在都不适用于本机:本机是 aarch64 macOS;std 在 Linux 上已经换成了 futex 实现(rust-src 1.99.1 的 sys/sync/mutex/mod.rs,本地无法核实是哪个版本换的),在 macOS 上仍是 pthread mutex。所以本卷不沿用这句话,而是在本机重新量:无竞争、不同线程数、临界区外不同工作量下的吞吐和尾延迟,结论依场景而定,见 和 std Mutex 比。0.13.0 的「先放锁再叫醒」又改变了有竞争时的结果,见 延后唤醒与守卫状态。

选 parking_lot 的理由,在今天更多是功能上的:1 字节的锁可以大量细粒度地用、const 构造、显式的公平控制、可升级读、Arc 守卫、死锁检测、lock_api 生态。这些取舍在 08 设计取舍 汇总。

小结

  • 停车场:锁只留状态位,等待队列集中到一张以地址为 key 的全局哈希表;思路来自 WebKit 的 WTF::ParkingLot,比 futex 多出「在队列锁里跑回调」。
  • 和 std 的分工:更小、不毒化、有显式公平控制、Condvar 无虚假唤醒且 requeue、可定制;std 在 macOS 上是 pthread,在 Linux 上是 futex,本机对比的是前者。
  • 三层:core(停车场)、lock_api(外壳,不依赖 core)、parking_lot(具体的锁)。
  • 本站经 tokio、dioxus、probe-rs、tracing、burn 等用到它;「比 std 快」是 x86_64 Linux 上对旧 std 的结论,本卷在本机重新测。

💬 关联讨论

发起讨论
加载中...