Rust in Everything
基于 parking_lot 0.12.5

怎么读这一卷

主线位置:四条主线的总览。本页说明全卷对着哪一份源码写、四条主线各经过哪些文件和章节、按什么顺序读,以及实验在什么机器上、按什么规矩做。
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版本tagtag 指向的提交
parking_lot0.12.5parking_lot-v0.12.5d7828ff「chore: release」,提交日期 2025-09-30
parking_lot_core0.9.12parking_lot_core-v0.9.12同上
lock_api0.4.14lock_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_slowraw_mutex.rs:65-75RawMutex 的一字节状态机
lock_slow:没上锁就抢;没人停车就 SpinWait::spin;置 PARKED_BIT;parkraw_mutex.rs:209-289同上;SpinWait
park:取本线程 ThreadData、锁桶、桶锁里跑 validate、入队、放桶锁、睡parking_lot.rs:590-706park 与四种 unpark
睡在哪:macOS 上是每线程一对 pthread mutex + 条件变量thread_parker/unix.rs:72-82ThreadParker 的各平台实现
unlock 的 CAS 失败 → unlock_slow → unpark_one,在桶锁里的回调中决定交接还是放锁raw_mutex.rs:291-323、parking_lot.rs:731-795park 与四种 unpark、eventual fairness

全程时序图:一次有竞争的加锁。

主线 2:一次 notify_all(Condvar 的 requeue)。

一步源码本卷
wait → wait_until_internal:以 condvar 地址为 key 停车,validate 记下配对的 mutex,before_sleep 解锁 mutexcondvar.rs:254-360Condvar
notify_all → notify_all_slow → unpark_requeue(condvar, mutex, …)condvar.rs:186-239同上
unpark_requeue:按桶下标顺序锁两个桶,叫醒至多一个,其余改 key、接到 mutex 的队尾parking_lot.rs:887-991park 与四种 unpark

全程时序图:一次 notify_all。

主线 3:全局停车场的一生。

一步源码本卷
线程第一次停车:ThreadData::new,NUM_THREADS += 1parking_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 集中讲。

阅读路线

摘自 parking_lot 和 std 的代码片段保留原来的格式(4 空格缩进),方便和原文件对照;本站自己写的实验代码用 2 空格缩进。

前置知识

概念本卷用到它的地方已有的讲解
Acquire / Release / Relaxed 的配对每个状态字、每次 CAScrossbeam 卷:内存序(必读)
「标志 + 互斥量 + 条件变量」的 park / unparkThreadParker、WordLockcrossbeam 卷:Parker 与 WaitGroup(必读)
先自旋、再让出、再阻塞的退避曲线SpinWaitcrossbeam 卷:Backoff
缓存行与伪共享哈希桶 #[repr(align(64))]crossbeam 卷:CachePadded
「先拿锁再登记」的不丢唤醒validate 在桶锁里执行rayon 卷:睡眠与唤醒的握手
std::sync::Mutex / Condvar 的用法全卷对照—
unsafe trait、PhantomData 与 Send / Synclock_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 比
E2Mutex 吞吐与尾延迟(0.12.5 / std / 0.13.0)和 std Mutex 比
E3解锁后自己又拿到锁的比例、最长连续独占公平解锁、bump 与 FairMutex;eventual fairness 用其中一组
E4读者不断时写者等多久和 std RwLock 比
E5同线程重复读在有写者排队时死锁读、写与写者优先
E6notify_all 的 requeue 与惊群Condvar;一次 notify_all 引用
E7停车场哈希表的扩容点停车场哈希表
E8死锁检测能抓到什么、开销多大死锁检测
E9守卫跨 .await 与 send_guardGuardSend 与 send_guard
E10Once 的快路径与毒化Once
E11SpinWait 每一步多长SpinWait
E12park → unpark 的唤醒延迟ThreadParker 的各平台实现
E130.13.0 对照列延后唤醒与守卫状态
E15两把 mutex 配一个 condvarCondvar

规划时还有一个 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 与 Oncerequeue;1 字节的 Once
06 lock_apiraw 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 包与工作区零差异;全卷链接统一用 tag parking_lot-v0.12.5。
  • 正文只写 0.12.5;0.13.0 的差别集中在 07。
  • 四条主线:一次有竞争的加锁、一次 notify_all、停车场的一生、桶锁与内存序横线。
  • 前置是 crossbeam 卷的内存序和 Parker;本卷不重讲。
  • 实验在 M2 Max(8P + 4E)上做,性能类 release、≥ 3 次取中位数、记录负载、每个实验带反向验证;计时实验一律不开 deadlock_detection。

💬 关联讨论

发起讨论
加载中...