Rust in Everything
基于 loom 0.7.2

在本站中

本站框架不直接也不间接依赖 loom:写作时 main 的 3292b81,主工作区 Cargo.lock 与网关 crates/gateway/Cargo.lock 里 grep -c loom 都是 0。loom 在各库里都是 [target.'cfg(loom)'.dev-dependencies] 或只在某个 cfg 下才激活的可选依赖,正常构建时不进入下游的依赖解析。这一页回答三个问题:依赖树里哪些 crate 用 loom 测过自己、本站要是给自己的代码开 --cfg loom 会发生什么、本站自己的并发代码值不值得加 loom。

flowchart TB
  site["本站 app(主工作区)"] --> tokio["tokio 1.52.3:cfg(all(test, loom))"]
  site --> bytes["bytes 1.12.0:cfg(all(test, loom))"]
  site --> tu["tokio-util 0.7.18:cfg(all(test, loom))"]
  site --> sqlx["sqlx-core 0.8.6"] --> el["event-listener 5.4.1:裸 cfg(loom)"]
  el --> cq["concurrent-queue 2.5.0:裸 cfg(loom)"]
  el --> pk["parking 2.2.1:cfg(all(loom, feature = loom))"]
  site --> ts["tracing-subscriber 0.3.23"] --> ss["sharded-slab 0.1.7:cfg(all(loom, any(test, feature = loom)))"]
  site -.->|"desktop"| ce["crossbeam-epoch 0.9.21:cfg(crossbeam_loom)"]
  site -.->|"search 模块"| os["oneshot 0.1.13:cfg(oneshot_loom)"]
  gw["网关(单独的 Cargo.lock)"] --> tokio
  gw --> b2["bytes 1.11.1"]
  gw --> ac["async-channel 2.5.0"] --> cq

依赖树里用 loom 测过自己的 crate

方法:把两个 Cargo.lock 里的 registry 包与本机 registry 源码对上,找 Cargo.toml 里声明了 loom 依赖、或源码里有 cfg(...loom...) 的包;谁把它带进来用 cargo tree --offline --locked -i <包> 反查(只解析 lock,不编译)。按名字匹配到的 bloom 一类误报已去掉。

crate主工作区网关切换条件loom 依赖loom 测试大致覆盖什么谁带进来
tokio1.52.31.52.3cfg(all(test, loom))dev-dependencysrc/ 下 16 个 loom_*.rs:sync 的 oneshot、mpsc、broadcast、watch、notify、rwlock、batch semaphore、atomic waker、list、set_once;runtime 的多线程 / 单线程调度、blocking、join set、local、oneshot直接依赖
tokio-util0.7.180.7.18cfg(all(test, loom))dev-dependencyloom_cancellation_token.rs 7 个:取消、父子 token、dropdioxus-fullstack、h2、reqwest 等;网关经 h2
bytes1.12.01.11.1cfg(all(test, loom))dev-dependencybytes.rs、bytes_mut.rs 各一个并发 clone 的模型axum、hyper、http、h2 等;网关经 pingora、h2、tokio
concurrent-queue2.5.02.5.0裸 cfg(loom)cfg(loom) 下的可选依赖,feature loomtests/loom.rs:bounded / unbounded 的 spsc、强制推入、容量event-listener;网关经 async-channel
event-listener5.4.15.4.2裸 cfg(loom)同上tests/loom.rs:notify、notify_additional、notify_all、监听者 drop 的几种时机sqlx-core;网关经 event-listener-strategy
parking2.2.12.2.1cfg(all(loom, feature = "loom"))同上tests/loom.rs 两个:smoke、yield_then_unparkevent-listener
sharded-slab0.1.7—cfg(all(loom, any(test, feature = "loom")))可选依赖 + dev-dependency(loom 0.5)src/tests/loom_slab.rs、loom_pool.rs:并发插入 / 删除 / 复用、引用计数守卫tracing-subscriber
crossbeam-utils0.8.210.8.21cfg(crossbeam_loom)可选依赖只有切换层,模型在 crossbeam-epochconcurrent-queue、crossbeam-queue
crossbeam-epoch0.9.21—cfg(crossbeam_loom) + feature loom可选依赖(改名 loom-crate)tests/loom.rs:it_works、Treiber 栈只在桌面构建里:dioxus-desktop → image → exr → rayon-core → crossbeam-deque
oneshot0.1.13—cfg(oneshot_loom)可选依赖tests/loom.rs 10 个:同步 / 异步收发、发送中 drop 接收端只在 search 模块:tantivy

另有几个只在 check-cfg 里声明了 loom 名字、没有 loom 代码的:crossbeam-channel / -deque / -queue(crossbeam_loom)、tokio-macros、tokio-test;tokio-stream 0.1.18 的 stream_map.rs 里有一个 cfg(loom) 的小模块,只换随机种子。

这些测试本站不会运行,它们是上游 CI 的一部分。本站能从中得到的,是知道「tokio 的 mpsc、watch、取消 token,sqlx 连接池底下的 event-listener」这些路径被上游用 loom 检查过,以及检查的形状(两三个线程、常数调小,写一个好模型)。「loom 通过」的含义和边界见 「loom 通过」意味着什么。各 crate 的切换层写法对照见 cfg(loom) 切换层;tokio 的写法在 tokio 卷源码地图 最后一节,bytes 的在 bytes 卷 clone 与提升(这两卷对着 tokio 1.54.1、bytes 1.12.1 写,切换条件与本站锁定的版本相同:本站的 tokio 1.52.3 src/loom/mod.rs、bytes 1.12.0 src/loom.rs 都是 cfg(all(test, loom)))。

本站要是开 --cfg loom

RUSTFLAGS 对整个依赖图生效。上表的切换条件分三类,结果不同:

  • cfg(all(test, loom))(tokio、tokio-util、bytes):依赖不是以 test 编译的,不切换,照常用 std。
  • 带 feature 或自己 cfg 名的(parking、sharded-slab、crossbeam-*、oneshot):不开对应的 feature 或 cfg 就不切换。
  • 裸 cfg(loom)(concurrent-queue、event-listener):一开就切换,而它们的 loom 依赖是 feature 控制的可选依赖。

实测(exp-loom/site-deps/,依赖 concurrent-queue =2.5.0 与 event-listener =5.4.1,即本站主工作区锁定的版本;一个普通的单元测试在模型外建一个队列):

构建结果
不加 flags通过
RUSTFLAGS="--cfg loom"concurrent-queue 编译失败:21 个错误,其中有「cannot find module or crate `loom` in this scope」(src/sync.rs:33-42 的几处 use loom::...)
同上,再打开两个依赖的 loom feature能编译;测试 panic「cannot access Loom execution state from outside a Loom model」

第二行是因为 cfg(loom) 让代码去 use loom::...,而 loom 这个可选依赖只有在依赖自己的 loom feature 打开时才被激活。本站的主工作区经 sqlx-core → event-listener → concurrent-queue、网关经 async-channel → concurrent-queue,都在这个位置上。所以本站将来要给自己的代码写 loom 测试,切换条件应当用自己的名字(例如 --cfg site_loom,像 crossbeam 的 crossbeam_loom、oneshot 的 oneshot_loom),而不是裸 loom;否则要么编不过,要么连带把这两个依赖换成 loom 类型,在模型外一用就 panic(坑:在模型外用 loom 类型)。

本站自己的并发代码

在主工作区与网关的 crates/ 下搜原子与显式内存序(grep -rnE 'sync::atomic|Atomic(Usize|U64|Bool)|Ordering::' --include='*.rs' crates),命中只有三处:

位置用法要不要 loom
crates/modules/moderation/src/llm_stage.rsstatic LLM_FAILURES: AtomicU64,fetch_add(1, Relaxed),只写进日志不要:计数器不保护任何数据,Relaxed 足够
crates/modules/admin/src/server.rs批量驳回时 8 路并发处理,AtomicU64 数成功条数,最后 load(Relaxed) 返回不要:for_each_concurrent(...).await 之后才读,等待本身提供了先后关系
crates/modules/payment/src/{pipeline,reconcile}.rs 的 #[cfg(test)]测试替身里的调用计数不要:只在测试里

没有一处用原子发布数据、没有自己写的无锁结构、没有 UnsafeCell 或手写的 Send / Sync。本站的并发全部交给 tokio、sqlx、dioxus 这些库,而它们的同步原语正是上表里被上游 loom 检查过的那部分。

在当前版本下不加 loom。 理由是证据而不是「暂时不需要」:没有任何代码靠内存序来保证正确性。重估的条件写成一条命令:

grep -rnE 'UnsafeCell|unsafe impl (Send|Sync)|compare_exchange|Ordering::(Acquire|Release|AcqRel)' --include='*.rs' crates

写作时它没有输出、退出码 1;在一个临时目录里放一行 flag.load(Ordering::Acquire) 再跑,命中并退出码 0(反向验证)。退出码变成 0,就说明有代码开始用原子发布数据或自己实现同步,那时按 11 动手 的做法给它加切换层和模型。这条门有意不含 SeqCst:支付模块的测试替身用 SeqCst 计数,算进去它现在就会亮;代价是将来只用 SeqCst 写的同步会漏过,评审时要另外留意。

小结

  • 本站两个 Cargo.lock 里没有 loom;依赖树里有 10 个 crate 带 loom 切换层或 loom 测试(tokio、tokio-util、bytes、concurrent-queue、event-listener、parking、sharded-slab、crossbeam-utils、crossbeam-epoch、oneshot),只在上游自己的 CI 里跑。
  • 切换条件三类:cfg(all(test, loom)) 不影响下游;带 feature / 自有 cfg 名的要显式打开;裸 cfg(loom) 一开就切换。
  • 实测:本站锁定的 concurrent-queue 2.5.0 在只加 --cfg loom 时编译失败;再开 loom feature 能编译,但在模型外使用就 panic。本站若要写 loom 测试,用自己的 cfg 名。
  • 本站自己的原子只有两个 Relaxed 计数器和测试替身,当前不需要 loom;出现 UnsafeCell、手写 Send / Sync、CAS 或 Acquire / Release 时再评估。

💬 关联讨论

发起讨论
加载中...