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::ParkingLotclass, 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.
拆成三点:
- 锁只剩状态位。
RawMutex是一个AtomicU8,只用最低两位:LOCKED_BIT和PARKED_BIT(「有人在等」)。无竞争时加锁、解锁各一次 CAS,从头到尾不碰停车场。 - 等待队列按地址找。 全进程只有一张哈希表,桶里挂着正在睡的线程。线程要等某把锁时,用锁的地址当 key 找到桶,把自己挂上去;解锁方用同一个地址找到同一个桶,摘一个线程叫醒。不同的锁可能落进同一个桶,桶里的链表按 key 区分。表的大小随线程数增长,而不是随锁的数量增长:锁再多,同一时刻在睡的线程也不会超过线程总数。
- 在队列锁里跑回调。 这是和 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.5 | std(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 是 futex | Linux 等平台用 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 feature | reload 层的 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::Mutexwas found to be 1.5x faster thanstd::sync::Mutexwhen 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 的结论,本卷在本机重新测。