Rust in Everything
基于 crossbeam-deque 0.8.8

crossbeam-deque

工作窃取调度器的标准配置是:每个线程一个自己的双端队列,自己从一头放、一头取;闲下来的线程到别人的队列另一头去「偷」;另外有一个所有人共享的全局队列,接收从外面提交进来的任务。crossbeam-deque 提供的正是这三样:Worker(自己的队列)、Stealer(别人手里的窃取端)、Injector(全局队列),外加一个表示窃取结果的 Steal<T>。

rayon 的线程池就建在它上面(rayon-core 1.13.0 的 registry.rs 第 10 行:use crossbeam_deque::{Injector, Steal, Stealer, Worker};)。读完这一部分再读 rayon 卷,调度循环里的每一次 pop / steal 都能对上这里的源码。

flowchart LR
  ext["外部线程 spawn"] -->|"push"| inj["Injector:64 槽一块的块链表"]
  subgraph t1["工作线程 1"]
    w1["Worker(push / pop)"]
  end
  subgraph t2["工作线程 2"]
    w2["Worker(push / pop)"]
  end
  inj -->|"steal / steal_batch_and_pop"| w1
  inj -->|"steal / steal_batch_and_pop"| w2
  w1 -.->|"Stealer::steal:从对端偷"| w2
  w2 -.->|"Stealer::steal:从对端偷"| w1

整个 crate 只有一个源文件 deque.rs,2261 行(含文档注释和例子),lib.rs 106 行是模块文档。按行号大致分成四块:

行内容章
1–163索引类型、常量、Buffer、InnerChase-Lev 双端队列、扩容与回收
164–1209Flavor、Worker、Stealer 及全部窃取方法同上,加 Steal、Retry 与批量窃取
1210–2078Slot、Block、InjectorInjector
2079–2261Steal<T> 与 FromIteratorSteal、Retry 与批量窃取

四章:

  1. Chase-Lev 双端队列:任务主线的主体。Inner{front, back, buffer},单主人的 Worker(!Sync)和可克隆的 Stealer;FIFO 与 LIFO 两种 flavor;push、pop、steal 逐行读,每个 Ordering 和 SeqCst fence 在和谁配对。实验 E5:用 loom 自写一个固定容量的版本,去掉任一处 SeqCst fence,loom 都报出同一个任务被取了两次。
  2. 扩容与回收:容量从 64 起翻倍、长度不到四分之一时减半;旧 buffer 交给 epoch,不小于 1 KiB 时立刻 flush;Buffer::read / write 用 volatile 访问,源码注释自认「技术上是 UB」。
  3. Steal、Retry 与批量窃取:Retry 为什么不能当 Empty(实验 E4);steal_batch 一次最多 32 个;FIFO 与 LIFO 之间批量窃取时的顺序;rayon 怎么处理 Retry。
  4. Injector:全局队列是 63 槽一块的无锁块链表,和 SegQueue、list channel 同构;HAS_NEXT 位;批量窃取直接搬进 Worker 的 buffer。

和 tokio 本地队列对照

tokio 卷的本地队列里讲过:tokio 在 2019 年以前用的就是 crossbeam 的 Chase-Lev deque,#1657 换成了固定 256 槽的环形缓冲。两边的取舍在这一部分的几章里会反复出现,先列在这里:

crossbeam-deque Workertokio 本地队列
容量从 64 起按 2 的幂增长,可缩小固定 256
满了扩容,旧 buffer 交给 epoch 回收把一半挪进全局注入队列
主人从哪头取FIFO 或 LIFO,构造时选只从队头(FIFO),LIFO 效果靠外面的一个槽
窃取一次一个,或一批(最多 32)一次偷一半,两次 CAS
回收旧内存需要 epoch(每次 steal 都 pin)不需要(buffer 不换)
全局队列Injector:无锁块链表Mutex + 侵入式链表(全局注入队列)
槽的读写volatile,非原子UnsafeCell + ptr::read,靠两段式认领避免并发读写同一槽

本部分的实验

#目录看什么章
E4exp-crossbeam/survey/(规划阶段)、exp-crossbeam/deque-retry/窃取不丢不重;把 Retry 当 Empty 的后果;steal 与 steal_batch_and_pop 的调用次数、Retry 次数和耗时Steal、Retry 与批量窃取
E5exp-crossbeam/loom-chase-lev/用 loom 原子写一个固定容量的 Chase-Lev,完整版通过,去掉 pop 或 steal 的 SeqCst fence 都被抓到Chase-Lev 双端队列

crossbeam-deque 本身不能在 loom 下跑:它用的是 std::sync::atomic,没有 epoch 和 utils 那样的 crate::primitive 切换层。所以 E5 只能自写模型。实验机器 M2 Max(8P + 4E),macOS,rustc 1.99.1,--release。

发布之后

main 上的 deque.rs 和 0.8.8 有一百多行差异,逐行看都是写法上的:std:: 换成 core:: / alloc::、unsafe fn 里补显式 unsafe {}(unsafe_op_in_unsafe_fn)、(len + 1) / 2 换成 div_ceil(2)、Block::new 改走一个统一的分配辅助函数(0240f82d「Add helper for safer allocation」)。没有改动算法或内存序。上游仓库另有两条把 volatile 访问换成「按字节原子 memcpy」的分支,都没有合并进 main,见扩容与回收。

💬 关联讨论

发起讨论
加载中...