Rust in Everything
基于 parking_lot_core 0.9.12

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["死锁检测:遍历所有桶建等待图"]

七章:

  1. 停车场哈希表:主线 3 全部。HashTable 与 Bucket 的布局、斐波那契哈希、ThreadData 何时创建、grow_hashtable 锁住所有桶再换表、三种 lock_bucket* 的「锁后确认表没换」、旧表永不释放;实测扩容发生在第 6、11、22、43 个停车线程。
  2. WordLock:停车场自己的锁:桶锁不能用停车场实现自己,所以一个 usize 里放两位标志加一个指向队列头的指针,入队无锁、出队在「队列锁」里补 prev 指针。
  3. park 与四种 unpark:主线 4 集中讲。park 的六个参数与三种结果、超时路径为什么要重新锁桶、unpark_one / unpark_all / unpark_requeue / unpark_filter、UnparkResult 四个字段、回调约束、「unpark_lock 在锁内、unpark 在锁外」。
  4. ThreadParker 的各平台实现:macOS 的 pthread 互斥量 + 条件变量、Linux 的 futex、Windows 的两种后端、其余平台一张表;和 std thread::park(macOS 上是 libdispatch 信号量)对照;实测唤醒延迟和栈顶符号。
  5. SpinWait:10 步,前 3 步自旋 2、4、8 次,后 7 步 yield_now;谁在用;和 crossbeam Backoff 对照;实测每一步多长。
  6. eventual fairness:每个桶一个计时器,0–1 ms 随机间隔强制一次公平交接;只在叫醒了线程时才计算;实测把单线程连续独占限制在约 1 ms。
  7. 死锁检测:每个线程记自己持有的锁,check_deadlock 遍历所有桶建等待图找环;看不到哪些情况;实测检测到两线程环和三线程环,以及这个 feature 的开销。

不重讲的部分

文档与代码的出入

源码注释和文档是资料,以代码为准。本部分遇到的几处:

文档的说法代码在哪章
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 时,没叫醒任何线程也会置 truepark 与四种 unpark
release_resource:「资源已释放或不是本线程获取的就 panic」找不到时静默忽略,注释解释是为 TLS 析构期间留的(parking_lot.rs:1230-1242)死锁检测

演进

只列 git log 可查的事实。

时间提交变化
2016-05-115cc3a5d「Initial commit」全局哈希表、LOAD_FACTOR、斐波那契哈希从第一个提交就在
2016-05-213bd8217「Add requeuing support for Condvar::notify_all」unpark_requeue
2016-05-2646d7618「Make the WordLock queue lock-free instead of using a spinlock」桶锁的等待队列改成无锁入队
2016-08-259322f98「Add support for eventual fairness and fair mutex unlocking」FairTimeout、be_fair、unlock_fair
2016-08-2679af298「Use a task-fair locking policy for RwLock」unpark_filter 与 ParkToken
2017-03-21c28f656「Add support for WaitOnAddress/WakeByAddress on Windows」Windows 第二个后端
2017-08-29bf63772「Implement deadlock detection」死锁检测
2018-12-018bcfae3「Create ThreadParker::IS_CHEAP_TO_CONSTRUCT」futex 平台的 WordLock 可以在栈上建 ThreadData
2021-08-07 / 086e3c66e「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,负载写在对应表格旁。

#目录看什么章
E7exp-parking_lot/hashtable-grow计数分配器观察每张表的大小和扩容点,开 / 不开 deadlock_detection 两组停车场哈希表
E8exp-parking_lot/deadlock-detect两线程、三线程环与几种盲区;开 / 不开 feature 的单线程与多线程开销死锁检测
E11exp-parking_lot/spinwait-curveSpinWait::spin 每一步的耗时,空载与 12 个忙线程两组SpinWait
E12exp-parking_lot/park-latency、park-stackparking_lot_core / std / crossbeam 三种 park 的乒乓延迟;睡着时的栈顶ThreadParker 的各平台实现
E3(一组)exp-parking_lot/fairness-nodd2 线程长临界区下单线程最长连续独占,0.12.5 与 0.13.0eventual fairness
E14exp-parking_lot/bucket-sharing相邻桶共用缓存行是否干扰:测不出,已删,数字留在哈希表一章停车场哈希表

💬 关联讨论

发起讨论
加载中...