crossbeam-epoch
无锁数据结构有一个绕不开的问题:一个节点从结构里摘下来以后,什么时候可以 free?别的线程可能刚刚读到它的指针,还没来得及解引用。crossbeam-epoch 用基于纪元的回收(epoch-based reclamation,EBR)回答这个问题:读者在访问共享指针之前把自己「钉」在当前纪元上(pin),写者把摘下的节点交给 defer,等全局纪元往前走两步、所有可能看见它的读者都已经离开,再真正析构。
这一部分是 05 crossbeam-deque 的前置:deque 扩容时换下来的旧缓冲区就是交给 epoch 回收的;deque 又是 rayon 调度的底座。crossbeam-skiplist 也建在它上面(07 并发跳表)。
flowchart LR why["为什么需要回收"] --> ptr["Atomic、Owned、Shared"] ptr --> pin["pin 与 Guard"] pin --> defer["defer 与回收"] defer --> lq["内部链表和队列"] lq --> col["Collector 与 LocalHandle"] defer -.->|"旧 buffer"| deque["05 deque 扩容"]
六章:
- 为什么需要内存回收:use-after-free 与 ABA;引用计数、hazard pointer、EBR 三种做法在读侧和回收侧各付什么代价;crossbeam 选 EBR 的后果。
- Atomic、Owned、Shared:三种指针的所有权语义,
Shared<'g, T>的生命周期为什么绑在Guard上;低位标签;Pointable让变长数组也能放进原子指针。 - pin 与 Guard:回收主线前半。
Epoch的位布局、Local::pin里那个 SeqCst fence、x86 上用 CAS 代替 fence 的「HACK」、每 128 次 pin 回收一次;线程本地HANDLE;unprotected()。 - defer 与回收:回收主线后半。
Deferred→ 64 个一袋的Bag→ 封上纪元进全局队列 →try_advance→ 「差两个纪元」才析构;每次最多 8 袋。2020 年「改成三个纪元」又在 5 天后回滚的那段历史。实验 E3。 - 内部的无锁链表和队列:存所有参与者的 Michael 链表、存垃圾袋的 Michael-Scott 队列,以及它们自己也靠 epoch 回收节点的「自举」。
- Collector 与 LocalHandle:自定义收集器、参与者的注册和注销、线程退出时残留垃圾的去向。实验 E5 的第二部分:在 loom 下跑 crossbeam-epoch 本身。
回收主线一览
这一部分对应一块内存的回收那张时序图。下面每一步标出在哪一章讲:
| 步骤 | 源码 | 章 |
|---|---|---|
epoch::pin() 取线程本地 HANDLE | default.rs:39 | 3 |
Local::pin:写本地纪元 + SeqCst fence;每 128 次 pin 调一次 collect | internal.rs:399 | 3 |
Atomic::swap 拿到旧的 Shared | atomic.rs:443 | 2 |
guard.defer_destroy(old) → Local::defer 放进本地 Bag | guard.rs:268、internal.rs:378 | 4 |
袋子满 64 个 → push_bag 封上全局纪元,进全局队列 | internal.rs:182 | 4、5 |
collect → try_advance:遍历所有参与者,都在当前纪元才 +1 | internal.rs:199、228 | 4、5 |
弹出 global - bag.epoch >= 2 的袋子,执行里面的析构,每次最多 8 袋 | internal.rs:148、208 | 4 |
文件
src/ 下的 Rust 文件,行数含文件内测试和文档注释:
| 文件 | 行数 | 管什么 | 章 |
|---|---|---|---|
atomic.rs | 1713 | Atomic、Owned、Shared、Pointable、标签位 | 2 |
internal.rs | 618 | Bag、SealedBag、Global、Local:pin、defer、collect、try_advance | 3、4、6 |
guard.rs | 523 | Guard、unprotected() | 3、4 |
sync/list.rs | 488 | 存 Local 的无锁侵入式链表 | 5 |
sync/queue.rs | 469 | 存 SealedBag 的 Michael-Scott 队列 | 5 |
collector.rs | 465 | Collector、LocalHandle | 6 |
lib.rs | 183 | 模块文档;crate::primitive 在 loom 和 std 之间切换 | 6 |
epoch.rs | 149 | Epoch、AtomicEpoch | 3 |
deferred.rs | 149 | Deferred:放进袋子的 FnOnce() | 4 |
default.rs | 93 | 默认全局收集器和线程本地 HANDLE | 3 |
sync/once_lock.rs | 88 | 给默认收集器用的 OnceLock | 3 |
合计 4945 行。crate 是 #![no_std]:alloc feature 打开上面除 default.rs 外的全部,std feature 再加上默认收集器和 pin() 这个自由函数。
本部分的实验
| # | 目录 | 看什么 | 章 |
|---|---|---|---|
| E3 | exp-crossbeam/epoch-rss/ | 一个线程长期 pin 住时垃圾一个都不回收,RSS 线性涨;flush 让它涨得更快;不 flush 时自动回收每次 pin 最多消化约 4 个对象 | defer 与回收 |
| E3' | exp-crossbeam/epoch-rss/(pin_cost) | 本机一次 pin 的开销 | pin 与 Guard |
| E5 | exp-crossbeam/epoch-loom/ | 在 --cfg crossbeam_loom 下编译 crossbeam-epoch 0.9.21,用 loom 检查「pin 住的读者读到的旧对象不会被析构」,以及去掉 pin 后 loom 能抓到 | Collector 与 LocalHandle |
实验机器是 M2 Max(8 性能核 + 4 能效核),macOS,rustc 1.99.1,--release。macOS 不能绑核,计时类结果受调度影响,每个数字都注明测量时的 1 分钟负载。
💬 关联讨论
发起讨论加载中...