Rust in Everything
基于 crossbeam-epoch 0.9.21

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 扩容"]

六章:

  1. 为什么需要内存回收:use-after-free 与 ABA;引用计数、hazard pointer、EBR 三种做法在读侧和回收侧各付什么代价;crossbeam 选 EBR 的后果。
  2. Atomic、Owned、Shared:三种指针的所有权语义,Shared<'g, T> 的生命周期为什么绑在 Guard 上;低位标签;Pointable 让变长数组也能放进原子指针。
  3. pin 与 Guard:回收主线前半。Epoch 的位布局、Local::pin 里那个 SeqCst fence、x86 上用 CAS 代替 fence 的「HACK」、每 128 次 pin 回收一次;线程本地 HANDLE;unprotected()。
  4. defer 与回收:回收主线后半。Deferred → 64 个一袋的 Bag → 封上纪元进全局队列 → try_advance → 「差两个纪元」才析构;每次最多 8 袋。2020 年「改成三个纪元」又在 5 天后回滚的那段历史。实验 E3。
  5. 内部的无锁链表和队列:存所有参与者的 Michael 链表、存垃圾袋的 Michael-Scott 队列,以及它们自己也靠 epoch 回收节点的「自举」。
  6. Collector 与 LocalHandle:自定义收集器、参与者的注册和注销、线程退出时残留垃圾的去向。实验 E5 的第二部分:在 loom 下跑 crossbeam-epoch 本身。

回收主线一览

这一部分对应一块内存的回收那张时序图。下面每一步标出在哪一章讲:

步骤源码章
epoch::pin() 取线程本地 HANDLEdefault.rs:393
Local::pin:写本地纪元 + SeqCst fence;每 128 次 pin 调一次 collectinternal.rs:3993
Atomic::swap 拿到旧的 Sharedatomic.rs:4432
guard.defer_destroy(old) → Local::defer 放进本地 Bagguard.rs:268、internal.rs:3784
袋子满 64 个 → push_bag 封上全局纪元,进全局队列internal.rs:1824、5
collect → try_advance:遍历所有参与者,都在当前纪元才 +1internal.rs:199、2284、5
弹出 global - bag.epoch >= 2 的袋子,执行里面的析构,每次最多 8 袋internal.rs:148、2084

文件

src/ 下的 Rust 文件,行数含文件内测试和文档注释:

文件行数管什么章
atomic.rs1713Atomic、Owned、Shared、Pointable、标签位2
internal.rs618Bag、SealedBag、Global、Local:pin、defer、collect、try_advance3、4、6
guard.rs523Guard、unprotected()3、4
sync/list.rs488存 Local 的无锁侵入式链表5
sync/queue.rs469存 SealedBag 的 Michael-Scott 队列5
collector.rs465Collector、LocalHandle6
lib.rs183模块文档;crate::primitive 在 loom 和 std 之间切换6
epoch.rs149Epoch、AtomicEpoch3
deferred.rs149Deferred:放进袋子的 FnOnce()4
default.rs93默认全局收集器和线程本地 HANDLE3
sync/once_lock.rs88给默认收集器用的 OnceLock3

合计 4945 行。crate 是 #![no_std]:alloc feature 打开上面除 default.rs 外的全部,std feature 再加上默认收集器和 pin() 这个自由函数。

本部分的实验

#目录看什么章
E3exp-crossbeam/epoch-rss/一个线程长期 pin 住时垃圾一个都不回收,RSS 线性涨;flush 让它涨得更快;不 flush 时自动回收每次 pin 最多消化约 4 个对象defer 与回收
E3'exp-crossbeam/epoch-rss/(pin_cost)本机一次 pin 的开销pin 与 Guard
E5exp-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 分钟负载。

💬 关联讨论

发起讨论
加载中...