怎么读这一卷
主线位置:四条主线的总览。本页说明全卷对着哪一份源码写、四条主线各经过哪些文件和章节、按什么顺序读,以及实验在什么机器上、按什么规矩做。
flowchart TB
start[从这里开始] --> q{读这一卷为了什么}
q -->|只想选一把锁| u["01 全景 → 03 和 std Mutex 比 → 09 常见坑"]
q -->|读懂停车场| s["02 parking_lot_core 全部"]
q -->|写自己的锁| w["06 lock_api → 02 park 协议 → 11 动手"]
q -->|全部| all["00 到 11 按顺序"]
锁定版本
parking_lot 仓库是一个工作区,三个发布 crate 各自编号,但 0.12.5 这一版是同一次发布:
| crate | 版本 | tag | tag 指向的提交 |
|---|---|---|---|
parking_lot | 0.12.5 | parking_lot-v0.12.5 | d7828ff「chore: release」,提交日期 2025-09-30 |
parking_lot_core | 0.9.12 | parking_lot_core-v0.9.12 | 同上 |
lock_api | 0.4.14 | lock_api-v0.4.14 | 同上 |
三个 tag 对象都在 2025-10-03(UTC)创建,解引用后是同一个提交。可以直接核实:
git for-each-ref --format='%(refname:short) %(*objectname:short)' \
refs/tags/parking_lot-v0.12.5 refs/tags/parking_lot_core-v0.9.12 refs/tags/lock_api-v0.4.14
# lock_api-v0.4.14 d7828ff
# parking_lot-v0.12.5 d7828ff
# parking_lot_core-v0.9.12 d7828ff
该提交下三个 Cargo.toml 的版本分别是 0.12.5 / 0.9.12 / 0.4.14。crates.io 上发布的包和仓库也一致:把 registry 里解压的 parking_lot-0.12.5/src、parking_lot_core-0.9.12/src、lock_api-0.4.14/src 分别和工作区的 src/、core/src/、lock_api/src/ 做 diff -r,输出为空。所以全卷的源码链接统一用 tag parking_lot-v0.12.5,形如 https://github.com/Amanieu/parking_lot/blob/parking_lot-v0.12.5/core/src/parking_lot.rs#L591-L706;frontmatter 里 core 的页面写 parking_lot_core 0.9.12,lock_api 的页面写 lock_api 0.4.14,其余写 parking_lot 0.12.5。
仓库的第一个提交是 2016-05-11(「Initial commit」),到 parking_lot-v0.12.5 共 928 个提交。三个 crate 的 MSRV 都是 1.71,edition 2021。
0.13.0 在哪。 tag parking_lot-v0.13.0 指向 1a5c3dd(2026-10-21),同时发布了 parking_lot_core 0.10.0 和 lock_api 0.5.0,和 0.12.5 之间隔了 89 个提交,MSRV 升到 1.95、edition 2024。改动涉及解锁顺序、raw trait 的签名、去掉 eventual fairness 和硬件锁省略等,几乎每一章都受影响。本卷的处理是:正文只写 0.12.5,遇到 0.13 不同的地方只放一句「0.13 起不同」加链接,所有差别集中在 07 0.13 的变化。0.13.0 的三个包也在本机 registry 里,可以离线编译,几个实验把它当对照组。
四条主线
每一页开头都会标出本页在哪条主线上。
主线 1:一次有竞争的 lock / unlock(Mutex)。
| 一步 | 源码 | 本卷 |
|---|---|---|
lock:一次 compare_exchange_weak(0 → LOCKED_BIT),失败进 lock_slow | raw_mutex.rs:65-75 | RawMutex 的一字节状态机 |
lock_slow:没上锁就抢;没人停车就 SpinWait::spin;置 PARKED_BIT;park | raw_mutex.rs:209-289 | 同上;SpinWait |
park:取本线程 ThreadData、锁桶、桶锁里跑 validate、入队、放桶锁、睡 | parking_lot.rs:590-706 | park 与四种 unpark |
| 睡在哪:macOS 上是每线程一对 pthread mutex + 条件变量 | thread_parker/unix.rs:72-82 | ThreadParker 的各平台实现 |
unlock 的 CAS 失败 → unlock_slow → unpark_one,在桶锁里的回调中决定交接还是放锁 | raw_mutex.rs:291-323、parking_lot.rs:731-795 | park 与四种 unpark、eventual fairness |
全程时序图:一次有竞争的加锁。
主线 2:一次 notify_all(Condvar 的 requeue)。
| 一步 | 源码 | 本卷 |
|---|---|---|
wait → wait_until_internal:以 condvar 地址为 key 停车,validate 记下配对的 mutex,before_sleep 解锁 mutex | condvar.rs:254-360 | Condvar |
notify_all → notify_all_slow → unpark_requeue(condvar, mutex, …) | condvar.rs:186-239 | 同上 |
unpark_requeue:按桶下标顺序锁两个桶,叫醒至多一个,其余改 key、接到 mutex 的队尾 | parking_lot.rs:887-991 | park 与四种 unpark |
全程时序图:一次 notify_all。
主线 3:全局停车场的一生。
| 一步 | 源码 | 本卷 |
|---|---|---|
线程第一次停车:ThreadData::new,NUM_THREADS += 1 | parking_lot.rs:175-193 | 停车场哈希表 |
grow_hashtable:桶数不到线程数 3 倍就锁住旧表所有桶、建新表、搬家、发布 | parking_lot.rs:261-314 | 同上 |
| 每次用桶:取表 → 锁桶 → 确认表没换 | parking_lot.rs:355-454 | 同上 |
这条主线没有单独的全景页,放在 停车场哈希表 开头。
主线 4:桶锁里能做什么、ThreadData 活多久、内存序。 不是一条调用链,而是三个贯穿全卷的问题。一是回调约束:validate、timed_out、callback 都在桶锁里执行,文档要求它们不 panic、不调用停车场的任何函数(parking_lot.rs:584-589)。二是生命周期:ThreadData 在线程局部存储里,线程退出就析构,所以叫醒方要在持桶锁时先 unpark_lock,再放桶锁、再真正叫醒。三是内存序:状态字上的很多操作是 Relaxed,因为改 PARKED_BIT 的时机被桶锁串行化了(raw_mutex.rs:36-54 的状态表注释)。每遇到一个 Relaxed,就说清它靠哪把锁或哪对 Acquire / Release 成立;分类沿用 crossbeam 卷的内存序一章。这一条在 park 与四种 unpark 集中讲。
阅读路线
- 只想选一把锁:01 全景,03 和 std Mutex 比,04 和 std RwLock 比,最后 09 常见坑。要升级到 0.13 的,加读 07。
- 读懂停车场:02 parking_lot_core 按顺序读。03–05 只用到 02 的结论(「
validate在桶锁里执行」「回调在桶锁里决定交接」「requeue 改 key」),可以先读 02 再挑着读。 - 写自己的锁:06 lock_api,park 与四种 unpark,11 动手。
摘自 parking_lot 和 std 的代码片段保留原来的格式(4 空格缩进),方便和原文件对照;本站自己写的实验代码用 2 空格缩进。
前置知识
| 概念 | 本卷用到它的地方 | 已有的讲解 |
|---|---|---|
| Acquire / Release / Relaxed 的配对 | 每个状态字、每次 CAS | crossbeam 卷:内存序(必读) |
| 「标志 + 互斥量 + 条件变量」的 park / unpark | ThreadParker、WordLock | crossbeam 卷:Parker 与 WaitGroup(必读) |
| 先自旋、再让出、再阻塞的退避曲线 | SpinWait | crossbeam 卷:Backoff |
| 缓存行与伪共享 | 哈希桶 #[repr(align(64))] | crossbeam 卷:CachePadded |
| 「先拿锁再登记」的不丢唤醒 | validate 在桶锁里执行 | rayon 卷:睡眠与唤醒的握手 |
std::sync::Mutex / Condvar 的用法 | 全卷对照 | — |
unsafe trait、PhantomData 与 Send / Sync | lock_api 的 raw trait 与守卫 | — |
不需要懂 async。和 tokio 的交集只在两处:tokio 的 parking_lot feature,以及守卫跨 .await(06 lock_api)。
实验环境与方法
本机:Apple M2 Max(8 个性能核 + 4 个能效核,12 个逻辑核),macOS,rustc 1.99.1,rust-src 同版本。生产服务器不做实验,所以凡是 Linux 上的行为(futex 后端),本卷只读代码、不实测。
实验程序都在 exp-parking_lot/ 下,每个实验一个 crate,从同一个模板复制:parking_lot =0.12.5、parking_lot_core =0.9.12、lock_api =0.4.14,对照用 crossbeam-utils 0.8.23、spin 0.9.9、tokio 1.54.1,以及单独 crate 里的 parking_lot 0.13.0。一律离线编译(cargo --offline --release),[profile.release] debug = 1 方便采样。
规矩和 crossbeam 卷、rayon 卷 相同:
- 每个实验写成「断言 → 方法 → 结果 → 反向验证」。 反向验证是故意造一个「应该失败」或「应该看不到效果」的输入,确认测量方法真的能分辨。
- 性能类一律
--release,每组至少重复 3 次取中位数,页面上给出每一轮的数字。macOS 不能把线程绑到指定核上,数字受调度影响。 - 记录负载。 计时开始前要求
sysctl -n vm.loadavg的 1 分钟和 5 分钟负载都低于 3,页面上写出测量时的负载。 - 线程数不超过 64。
- 正确性类(扩容点、检测到的环、报错原文、栈顶符号)在任何负载下都跑,至少 3 次,输出一致才写。
- 实验输出里的本机路径一律去掉。
deadlock_detection 不能出现在计时实验里。 开了这个 feature,每次 lock 后多一次 acquire_resource、unlock 前多一次 release_resource,都是对本线程 ThreadData 里一个 Vec<usize> 的 push 或查找(parking_lot.rs:1224-1242);而且每个加过锁的线程都会创建 ThreadData、推动哈希表扩容。实测(死锁检测的 E8)单线程无竞争的 lock + unlock 从约 5.3 ns 变成约 7.1 ns,12 线程高竞争时的变化方向甚至相反。所以计时实验一律复制成不开这个 feature 的 crate;只有观察扩容(E7)和死锁检测本身(E8)的实验打开它,并和不开的版本对照。
本卷的实验:
| # | 名字 | 在哪一页 |
|---|---|---|
| E1 | 各种锁的 size_of | 和 std Mutex 比 |
| E2 | Mutex 吞吐与尾延迟(0.12.5 / std / 0.13.0) | 和 std Mutex 比 |
| E3 | 解锁后自己又拿到锁的比例、最长连续独占 | 公平解锁、bump 与 FairMutex;eventual fairness 用其中一组 |
| E4 | 读者不断时写者等多久 | 和 std RwLock 比 |
| E5 | 同线程重复读在有写者排队时死锁 | 读、写与写者优先 |
| E6 | notify_all 的 requeue 与惊群 | Condvar;一次 notify_all 引用 |
| E7 | 停车场哈希表的扩容点 | 停车场哈希表 |
| E8 | 死锁检测能抓到什么、开销多大 | 死锁检测 |
| E9 | 守卫跨 .await 与 send_guard | GuardSend 与 send_guard |
| E10 | Once 的快路径与毒化 | Once |
| E11 | SpinWait 每一步多长 | SpinWait |
| E12 | park → unpark 的唤醒延迟 | ThreadParker 的各平台实现 |
| E13 | 0.13.0 对照列 | 延后唤醒与守卫状态 |
| E15 | 两把 mutex 配一个 condvar | Condvar |
规划时还有一个 E14(相邻哈希桶共用 128 字节缓存行是否互相干扰),实测连「两把锁落在同一个桶」这个上限对照都和「相距很远」分不开,方法看不出任何桶上的干扰,按计划删掉,停车场哈希表 里只保留布局事实和这次尝试的数字。
页面上的标注:「推测」表示从源码或数据推出来、但没有实验或文档直接支撑的说法;性能数字都注明测量条件。上游的 README、CHANGELOG、源码注释是资料,和代码不一致时以代码为准,页面里指出出入。
目录
| 部分 | 内容 |
|---|---|
| 00 导读 | 本页;源码地图 |
| 01 全景 | 停车场解决什么问题;一次有竞争的加锁;一次 notify_all |
| 02 parking_lot_core | 本卷核心:哈希表、WordLock、park 协议、ThreadParker、SpinWait、eventual fairness、死锁检测 |
| 03 Mutex 家族 | RawMutex、公平解锁与 bump、ReentrantMutex、和 std 比 |
| 04 RwLock | 状态字与两条队列、写者优先、可升级读、和 std 比 |
| 05 Condvar 与 Once | requeue;1 字节的 Once |
| 06 lock_api | raw trait、外壳与守卫、send_guard、自定义锁 |
| 07 0.13 的变化 | 先放锁再叫醒、守卫状态、去掉的与修正的 |
| 08 设计取舍 | 各章取舍汇总 |
| 09 常见坑 | 报错原文或现象做标题 |
| 10 在本站中 | 本站依赖链里的 parking_lot |
| 11 动手 | 最小停车场、信号量、自旋锁外壳 |
小结
- parking_lot 0.12.5、parking_lot_core 0.9.12、lock_api 0.4.14 是同一个提交
d7828ff,registry 包与工作区零差异;全卷链接统一用 tagparking_lot-v0.12.5。 - 正文只写 0.12.5;0.13.0 的差别集中在 07。
- 四条主线:一次有竞争的加锁、一次
notify_all、停车场的一生、桶锁与内存序横线。 - 前置是 crossbeam 卷的内存序和
Parker;本卷不重讲。 - 实验在 M2 Max(8P + 4E)上做,性能类 release、≥ 3 次取中位数、记录负载、每个实验带反向验证;计时实验一律不开
deadlock_detection。