parking_lot_core
parking_lot_core 回答一个问题:线程怎样以一个地址为 key 睡下、被叫醒,而不丢唤醒、不访问已释放的内存。 它不知道锁是什么。RawMutex、RawRwLock、Condvar、Once 都只是在它上面写了几个回调。核心是 parking_lot.rs 一个文件,非测试部分约 1430 行,其中死锁检测约 330 行;另外是桶锁 word_lock.rs(327 行)、各平台的 thread_parker/、spinwait.rs(74 行)。
flowchart TB user["RawMutex / RawRwLock / Condvar / Once"] -->|"park(key, validate, before_sleep, timed_out, token, timeout)"| park["park"] user -->|"unpark_one / all / requeue / filter"| unpark["unpark_*"] park --> td["with_thread_data:本线程 ThreadData(首次创建时可能扩表)"] park --> lb["lock_bucket:hash(key) → 桶 → WordLock"] unpark --> lb lb --> table["全局 HashTable:桶数 ≥ 3 × 线程数,只增不减"] park --> tp["ThreadParker:macOS pthread 条件变量 / Linux futex"] unpark -->|"桶锁里 unpark_lock,锁外 unpark"| tp unpark --> ft["每桶一个 FairTimeout:be_fair"] td -.-> dd["死锁检测:遍历所有桶建等待图"]
七章:
- 停车场哈希表:主线 3 全部。
HashTable与Bucket的布局、斐波那契哈希、ThreadData何时创建、grow_hashtable锁住所有桶再换表、三种lock_bucket*的「锁后确认表没换」、旧表永不释放;实测扩容发生在第 6、11、22、43 个停车线程。 - WordLock:停车场自己的锁:桶锁不能用停车场实现自己,所以一个
usize里放两位标志加一个指向队列头的指针,入队无锁、出队在「队列锁」里补prev指针。 - park 与四种 unpark:主线 4 集中讲。
park的六个参数与三种结果、超时路径为什么要重新锁桶、unpark_one/unpark_all/unpark_requeue/unpark_filter、UnparkResult四个字段、回调约束、「unpark_lock在锁内、unpark在锁外」。 - ThreadParker 的各平台实现:macOS 的 pthread 互斥量 + 条件变量、Linux 的 futex、Windows 的两种后端、其余平台一张表;和 std
thread::park(macOS 上是 libdispatch 信号量)对照;实测唤醒延迟和栈顶符号。 - SpinWait:10 步,前 3 步自旋 2、4、8 次,后 7 步
yield_now;谁在用;和 crossbeamBackoff对照;实测每一步多长。 - eventual fairness:每个桶一个计时器,0–1 ms 随机间隔强制一次公平交接;只在叫醒了线程时才计算;实测把单线程连续独占限制在约 1 ms。
- 死锁检测:每个线程记自己持有的锁,
check_deadlock遍历所有桶建等待图找环;看不到哪些情况;实测检测到两线程环和三线程环,以及这个 feature 的开销。
不重讲的部分
- park / unpark 的「标志 + 互斥量 + 条件变量」算法:crossbeam 卷的 Parker。本部分只讲
ThreadParker与它的差别:每线程一个、放在线程局部的ThreadData里、由停车场替调用方管理队列。 - 退避曲线:crossbeam 卷的 Backoff,SpinWait 只做对照。
- 缓存行大小与伪共享:crossbeam 卷的 CachePadded。
- 内存序的分类方法:crossbeam 卷的内存序一章。
文档与代码的出入
源码注释和文档是资料,以代码为准。本部分遇到的几处:
| 文档的说法 | 代码 | 在哪章 |
|---|---|---|
UnparkResult::be_fair:「平均每 0.5 ms 对任意给定的 key 置一次」 | 计时器 fair_timeout 在 Bucket 里,同一个桶里的不同 key 共用一个(parking_lot.rs:89-100) | eventual fairness |
UnparkResult::have_more_threads:「只在叫醒了线程时才可能为 true」 | unpark_filter 遇到 Skip / Stop 时、unpark_requeue 选 RequeueOne 时,没叫醒任何线程也会置 true | park 与四种 unpark |
release_resource:「资源已释放或不是本线程获取的就 panic」 | 找不到时静默忽略,注释解释是为 TLS 析构期间留的(parking_lot.rs:1230-1242) | 死锁检测 |
演进
只列 git log 可查的事实。
| 时间 | 提交 | 变化 |
|---|---|---|
| 2016-05-11 | 5cc3a5d「Initial commit」 | 全局哈希表、LOAD_FACTOR、斐波那契哈希从第一个提交就在 |
| 2016-05-21 | 3bd8217「Add requeuing support for Condvar::notify_all」 | unpark_requeue |
| 2016-05-26 | 46d7618「Make the WordLock queue lock-free instead of using a spinlock」 | 桶锁的等待队列改成无锁入队 |
| 2016-08-25 | 9322f98「Add support for eventual fairness and fair mutex unlocking」 | FairTimeout、be_fair、unlock_fair |
| 2016-08-26 | 79af298「Use a task-fair locking policy for RwLock」 | unpark_filter 与 ParkToken |
| 2017-03-21 | c28f656「Add support for WaitOnAddress/WakeByAddress on Windows」 | Windows 第二个后端 |
| 2017-08-29 | bf63772「Implement deadlock detection」 | 死锁检测 |
| 2018-12-01 | 8bcfae3「Create ThreadParker::IS_CHEAP_TO_CONSTRUCT」 | futex 平台的 WordLock 可以在栈上建 ThreadData |
| 2021-08-07 / 08 | 6e3c66e「Fix Ordering on WordLock (tsan detected)」、4fd88be「Detect and work around TSan's lack of support for fences」 | WordLock 的内存序和 fence |
0.13.0(parking_lot_core 0.10.0)在本部分涉及的改动:去掉 eventual fairness(#533 中的 8cfdedb)、SpinWait 的步数从 1 开始(#540)、WordLock 解锁重试(#533 中的 fb08e42)、Apple 上的超时改为相对等待(#533 中的 c175220)、generic 后端不再忙等,汇总在 07 0.13 的变化。
本部分的实验
机器 M2 Max(8P + 4E),macOS,rustc 1.99.1。计时类在不开 deadlock_detection 的 crate 里跑,开始时 1 分钟与 5 分钟负载都低于 3,负载写在对应表格旁。
| # | 目录 | 看什么 | 章 |
|---|---|---|---|
| E7 | exp-parking_lot/hashtable-grow | 计数分配器观察每张表的大小和扩容点,开 / 不开 deadlock_detection 两组 | 停车场哈希表 |
| E8 | exp-parking_lot/deadlock-detect | 两线程、三线程环与几种盲区;开 / 不开 feature 的单线程与多线程开销 | 死锁检测 |
| E11 | exp-parking_lot/spinwait-curve | SpinWait::spin 每一步的耗时,空载与 12 个忙线程两组 | SpinWait |
| E12 | exp-parking_lot/park-latency、park-stack | parking_lot_core / std / crossbeam 三种 park 的乒乓延迟;睡着时的栈顶 | ThreadParker 的各平台实现 |
| E3(一组) | exp-parking_lot/fairness-nodd | 2 线程长临界区下单线程最长连续独占,0.12.5 与 0.13.0 | eventual fairness |
| E14 | exp-parking_lot/bucket-sharing | 相邻桶共用缓存行是否干扰:测不出,已删,数字留在哈希表一章 | 停车场哈希表 |
💬 关联讨论
发起讨论加载中...