同步内存序检查
sync-lint 是仓库里的一个静态检查工具,入口命令是:
cargo xtask sync-lint
它的目标是:检查Atomic原子变量的使用,抓那些承担同步语义、但仍然写成了 Relaxed 的用法。
典型情况包括:
- 这个原子变量是不是在决定“别的线程/任务/CPU 能不能继续执行”
- 它是不是在负责“发布状态,再唤醒别人”
如果是,那么用 Relaxed 就有可能过弱,尤其是在 AArch64、RISC-V 这类弱内存序架构上。
当前检查范围
1. 在等待条件里使用 Relaxed
当前实现主要检查两种高置信等待场景:
wait_until/wait_timeout_until/wait_while这类等待接口的条件闭包- 带有明显阻塞或让出动作的
while循环条件,例如循环体里出现thread::yield_now()、thread::sleep(...)、spin_loop()或park()
这类代码会被报告:
WQ.wait_until(|| COUNTER.load(Ordering::Relaxed) == NUM_TASKS);
while !READY.load(Ordering::Relaxed) {
thread::yield_now();
}
原因是这里的原子变量不是“看一眼统计值”,而是在决定:
- 当前线程是否继续等待
- 当前阶段是否已经完成
- 另一个执行流是否已经把状态准备好
2. 用 Relaxed 写状态后立刻 notify / wake
这类代码也会被报告:
COUNTER.fetch_add(1, Ordering::Relaxed);
WQ.notify_one(true);
GO.store(true, Ordering::Relaxed);
WQ.notify_all(true);
这种模式通常表示:
- 先发布状态
- 再唤醒等待者去观察这个状态
如果发布动作还是 Relaxed,等待者就可能虽然被唤醒了,但看不到你刚刚写入的最新状态。
3. 同一个同步原子混用了强序和 Relaxed
如果某个原子已经被检查器认定为“同步变量”,比如:
- 它出现在等待条件里
- 它承担了“写状态后唤醒别人”的职责
那么检查器还会继续看这个原子在同一文件里的其他访问。
如果它一边用了:
AcquireReleaseAcqRelSeqCst
另一边又用了:
Relaxed
那么这些 Relaxed 访问也会被报告。
例如:
READY.store(true, Ordering::Release);
WQ.notify_all(true);
if READY.load(Ordering::Relaxed) {
do_work();
}
这类情况通常说明:这个原子已经明显承担同步语义,但仍然有一部分访问保留在 Relaxed,需要重新确认是否真的足够。
当前不检查什么
为了避免误报,当前实现依然刻意没有做“大而全”的规则。
目前不会主动检查这些情况:
- 纯统计/计数用途的原子变量;但如果一个“计数器”本身参与阶段同步、等待条件或唤醒流程,它仍然可能被报告
Acquire/Release是否成对匹配AcqRel或SeqCst是否过强- 更复杂的跨函数发布模式
- 基于任意控制流和任意数据结构的通用 dataflow 推理
- lock-free 算法内部的状态机细节
也就是说,当前规则是一个保守版、低误报检查器。
典型提示长什么样
提示格式是:
<path>:<line>:<column>: <message> [<rule>]
例如:
test-suit/arceos/rust/task/wait_queue/src/main.rs:44:13:
Relaxed atomic write is immediately followed by a wake/notify operation
[suspicious_relaxed_publish_before_notify]
或者:
test-suit/arceos/rust/task/parallel/src/main.rs:40:12:
Relaxed atomic load is used in a wait condition
[suspicious_relaxed_wait_condition]
也可能看到:
some/path.rs:27:8:
Relaxed atomic access is mixed with stronger orderings on the same synchronization variable
[suspicious_relaxed_mixed_ordering]
这些提示分别是什么意思
suspicious_relaxed_wait_condition
意思是:
- 某个
Atomic*的load(Ordering::Relaxed)被拿来做等待/阻塞/自旋条件 - 这个值不再只是“统计信息”
- 而是在决定当前执行流能不能往前推进
一般应考虑把这类读改成:
Ordering::Acquire
对应的写一侧如果也承担发布语义,通常要改成:
Ordering::Release
suspicious_relaxed_publish_before_notify
意思是:
- 你刚刚对原子变量做了
Relaxed写入 - 然后马上
notify_one/notify_all/wake
这通常是在“告诉别人状态已经准备好”。
这类写一般应考虑改成:
Ordering::Release
suspicious_relaxed_mixed_ordering
意思是:
- 同一个原子在同一文件里既出现了强序访问,也出现了
Relaxed访问 - 并且这个原子已经有足够证据表明自己是同步变量,而不是纯统计值
这类提示通常不是单独成立的“文本匹配”,而是结合了前面的等待/唤醒语义一起判断出来的。
一般应先回头看这个原子的职责,再决定是否把相关 Relaxed 访问也统一成:
- 读侧
Ordering::Acquire - 写侧
Ordering::Release
一般怎么修
一个简单的经验法则是:
- 读侧如果在“等待某个状态成立”,优先考虑
Acquire - 写侧如果在“发布这个状态, 然后唤醒别人”,优先考虑
Release
最常见的修法是把:
flag.store(true, Ordering::Relaxed);
改成:
flag.store(true, Ordering::Release);
把:
while !flag.load(Ordering::Relaxed) {
thread::yield_now();
}
改成:
while !flag.load(Ordering::Acquire) {
thread::yield_now();
}
一个完整例子
下面这个模式很典型:
COUNTER.fetch_add(1, Ordering::Relaxed);
WQ1.notify_one(true);
WQ1.wait_until(|| COUNTER.load(Ordering::Relaxed) == NUM_TASKS);
它的问题不是“COUNTER 是计数器,所以一定错”,而是:
- 这个计数器不只是统计用途
- 它实际上承担了“阶段同步”的职责
- 主线程靠它判断是否所有 worker 都到齐了
- worker 在更新它之后立刻唤醒等待者
因此它更像“同步变量”,而不是普通计数器。
更合适的写法是:
COUNTER.fetch_add(1, Ordering::Release);
WQ1.notify_one(true);
WQ1.wait_until(|| COUNTER.load(Ordering::Acquire) == NUM_TASKS);
这里的含义是:
Release:发布“我已经到达这个阶段”Acquire:观察者在判断阶段完成时,要看到这个发布过的状态
如果你认为这是误报
第一阶段规则已经比较保守,但仍然保留了显式忽略入口。
可以在代码上方写:
// sync-lint: ignore suspicious_relaxed_wait_condition
或者:
// sync-lint: ignore suspicious_relaxed_publish_before_notify
或者:
// sync-lint: ignore suspicious_relaxed_mixed_ordering
也可以写成通用忽略:
// sync-lint: ignore
当前实现的匹配规则要点是:
- 注释里必须至少包含
sync-lint: ignore - 如果后面再带具体规则名,就只忽略那一条规则
- 如果不带具体规则名,就会把当前
sync-lint的规则都忽略掉 - 只写
// sync-lint:这种前缀并不会生效 - 忽略注释必须位于被报告代码上方的 1 到 3 行内
例如:
// sync-lint: ignore suspicious_relaxed_wait_condition
wq.wait_until(|| counter.load(Ordering::Relaxed) == 1);
表示只忽略 suspicious_relaxed_wait_condition。
// sync-lint: ignore
wq.wait_until(|| counter.load(Ordering::Relaxed) == 1);
表示通用忽略。
// sync-lint:
wq.wait_until(|| counter.load(Ordering::Relaxed) == 1);
这不会被识别成忽略注释。
如果你确实要忽略,建议把理由写清楚,例如:
// sync-lint: ignore suspicious_relaxed_wait_condition
// stats-only counter, not used for synchronization
请注意,只有在你能明确说明“这个原子变量不承担同步语义”时,才建议这样做。
提交前建议
如果你改动了并发、任务、等待队列、唤醒、原子状态机等相关逻辑,建议在本地先跑一遍:
cargo xtask sync-lint
这样能比 CI 更早发现问题,也更方便你在本地直接跳到报错位置修改。
结论
可以把 sync-lint 理解为一个简单的问题筛子:
- 它不试图判断所有原子变量
- 它只抓那些“看起来像同步变量,但用了过弱内存序”的高置信模式
如果你的代码被它挡住,先不要把它理解成“工具不允许用 Relaxed”,而应该先问自己:
- 这个原子变量是不是在控制别的线程/任务/CPU 的推进时机?
- 它是不是在发布一个会被 别人观察到的阶段状态?
如果答案是“是”,那么优先考虑把它改成 Acquire/Release 风格的同步。