fence 与 SeqCst
主线位置:主线 2 与主线 4 的交点。fence 改变的是线程时钟(向量时钟与同步点),从而改变后续 load 的候选(load 的候选);SeqCst 是 loom 内存模型里取舍最大的一处,README 把它列为第一条「Unsupported features」。
四种 fence
loom::sync::atomic 只导出 fence,没有 compiler_fence(sync/atomic/mod.rs:25-28);crossbeam 在 loom 下用 fence 代替 compiler_fence,见 crossbeam 卷的怎么验证并发代码。运行时实现 rt/atomic.rs:166-209:
pub(crate) fn fence(ordering: Ordering) {
rt::synchronize(|execution| match ordering {
Ordering::Acquire => fence_acq(execution),
Ordering::Release => fence_rel(execution),
Ordering::AcqRel => fence_acqrel(execution),
Ordering::SeqCst => fence_seqcst(execution),
Ordering::Relaxed => panic!("there is no such thing as a relaxed fence"),
order => unimplemented!("unimplemented ordering {:?}", order),
});
}
fn fence_acq(execution: &mut Execution) {
// Find all stores for all atomic objects and, if they have been read by
// the current thread, establish an acquire synchronization.
for state in execution.objects.iter_mut::<State>() {
// Iterate all the stores
for store in state.stores_mut() {
if !store.first_seen.is_seen_by_current(&execution.threads) {
continue;
}
store
.sync
.sync_load(&mut execution.threads, Ordering::Acquire);
}
}
}
fn fence_rel(execution: &mut Execution) {
// take snapshot of cur view and record as rel view
let active = execution.threads.active_mut();
active.released = active.causality;
}
| fence | 做什么 |
|---|---|
Acquire | 遍历所有原子对象窗口里的所有写,凡是「已被当前线程看到」(is_seen_by_current)的,都对它的同步点做一次 acquire。之前的 Relaxed 读本身不传递时钟,到 fence 这里一次补齐 |
Release | 把当前 causality 整个存进线程的 released。之后每一次写,sync_store 都先并入 released,所以 fence 之后的 Relaxed 写也带着 fence 之前的一切 |
AcqRel | 先 Acquire 再 Release |
SeqCst | AcqRel,再加 seq_cst_fence(下一节) |
Relaxed | panic「there is no such thing as a relaxed fence」(实测原文)。std 写成字面量 fence(Relaxed) 时编译期就被 invalid_atomic_ordering 拒绝,运行时传入 Relaxed 也 panic 同一句话;这个 lint 只认 std 的函数,loom 的 fence(Relaxed) 能编译,到运行时才 panic |
两个细节:
- fence 不是调度点。 它只调
rt::synchronize(自增时钟),不调branch。fence 前后能不能切到别的线程,取决于前后的原子操作。 fence_acq的代价和模型里原子对象的个数成正比(每个对象最多 7 次写),fence(SeqCst)也包含这一步。
实验(exp-loom/memory-model/tests/fences.rs 的 mp_fences):消息传递,数据是 UnsafeCell,flag 的读写都用 Relaxed,靠 fence 同步。
| 写法 | 结果 |
|---|---|
写方 fence(Release),读方 fence(Acquire) | 6 个交错通过 |
只有读方 fence(Acquire) | 第 3 个交错「Causality violation: Concurrent read and write accesses.」 |
只有写方 fence(Release) | 第 3 个交错,同上 |
上游的三个 fence_sw_* 测试(tests/fence.rs:10-75)是同一组:fence 对 fence、release 写对 acquire fence、release fence 对 acquire 读,对应 CDSChecker 图 10 的三条 Fence Synchronization 规则。
fence(SeqCst):一个全局时钟
pub(crate) fn seq_cst_fence(&mut self) {
self.threads[self.active.unwrap()]
.causality
.join(&self.seq_cst_causality);
self.seq_cst_causality
.join(&self.threads[self.active.unwrap()].causality);
}
seq_cst_causality 是 thread::Set 上的一个全局时钟,每轮清零。每执行一次 fence(SeqCst),当前线程先并入它、再把自己并回去。效果:loom 执行顺序里后执行 fence(SeqCst) 的线程,得到先执行的线程在它那道 fence 之前的全部因果历史。
以 SB(store buffering)为例:线程 A x = 1; fence(SeqCst); 读 y,线程 B y = 1; fence(SeqCst); 读 x。不论哪个先执行 fence,后执行的那一方都通过全局时钟知道了对方的写;对方的写已经「被看到」,按 load 的候选 的规则一,更老的初值 0 不再是候选,这一方只能读到 1。所以 (0, 0) 消失。
sequenceDiagram participant A as 线程 A participant G as seq_cst_causality participant B as 线程 B A->>A: x.store(1, Relaxed) A->>G: fence(SeqCst):A 并入 G,G 并入 A B->>B: y.store(1, Relaxed) B->>G: fence(SeqCst):B 并入 G(得到 A 的时钟) B->>B: x.load:x = 1 已被看到,初值 0 被排除 A->>A: y.load:可能读到 0 或 1
SeqCst 访问:seq_cst() 是空函数
Synchronize 在 SeqCst 读写时额外调用 threads.seq_cst()(向量时钟与同步点),它的实现是 rt/thread.rs:319-341:
/// Insert a point of sequential consistency
/// TODO
/// - Deprecate SeqCst accesses and allow SeqCst fences only. The semantics of SeqCst accesses
/// is complex and difficult to implement correctly. On the other hand, SeqCst fence has
/// well-understood and clear semantics in the absence of SeqCst accesses, and can be used
/// for enforcing the read-after-write (RAW) ordering which is probably what the user want to
/// achieve with SeqCst.
/// ...
pub(crate) fn seq_cst(&mut self) {
// The previous implementation of sequential consistency was incorrect (though it's correct
// for `fence(SeqCst)`-only scenario; use `seq_cst_fence` for `fence(SeqCst)`).
// As a quick fix, just disable it. This may fail to model correct code,
// but will not silently allow bugs.
}
所以在 0.7.2 里,SeqCst 的 load / store / RMW 和 AcqRel 只差一处:SeqCst load 跳过被更新的 SeqCst 写压着的 SeqCst 写(atomic.rs:766-769,实验见 load 的候选 的规则三)。README 的说法与此一致(README.md:67-79):
SeqCstaccesses (e.g.load,store, ..): They are are regarded asAcqRel. That is, they impose weaker synchronization, causing Loom to generate false alarms (not complete). … On the other hand,fence(SeqCst)is supported.
方向是假阳性:正确依赖 SeqCst 访问的代码可能被 loom 报错;不会因此放过错误的代码。
调用 seq_cst() 的地方因此都是空操作:Synchronize 的 SeqCst 分支、loom::sync::Mutex 构造时传 seq_cst = true(sync/mutex.rs:25,解锁处注释「Establish sequential consistency between the lock's operations」,rt/mutex.rs:70-73)、RwLock 的四处(rt/rwlock.rs 第 101、129、202、240 行)、JoinHandle 用的 Notify(seq_cst = true)。锁的同步只剩 acquire / release,见 05 Mutex 与 RwLock。
这条注释的来历
git log -S 查「As a quick fix, just disable it」,指向 fd8680c「Relax how SeqCst is modelled」(#108,2020-02-16,0.2.15)。那次提交把 seq_cst() 从「对全局时钟双向 join」改成空函数,提交说明写:原实现提供了全局 happens-before,太强;作为权宜之计先关掉,代价是不能验证正确依赖 SeqCst 的算法,但不会放过有错的代码。
注释里提到的 seq_cst_fence 是 19 个月之后才有的:ffb2672「Implement Release, AcqRel, and SeqCst fences」(#220,2021-09-22,0.5.2)新增了 seq_cst_fence,内容正是 #108 删掉的那段双向 join,只用在 fence(SeqCst) 上;同一个提交给注释补了括号里那句「though it's correct for fence(SeqCst)-only scenario」、加了上面的 TODO 与参考文献(SCFix 论文、P0668R5、Promising Semantics),并在 README 增加「Unsupported features」一节。在 #220 之前,loom 只支持 fence(Acquire),其他序 assert_eq! 失败(f8ef5e1 #57 引入)。
三个 litmus
全部只用两个或三个原子变量,结果用 std 的 Mutex<BTreeSet> 在模型外收集。不设抢占上限。
SB:store buffering(E5)
两个线程各写自己的变量、再读对方的变量。目录 exp-loom/survey/tests/litmus.rs 与 exp-loom/memory-model/tests/fences.rs 的 sb_one_sided_fence。
| 写法 | 交错数 | (A 读到, B 读到) |
|---|---|---|
Relaxed 写、Relaxed 读 | 30 | {(0,0), (0,1), (1,0), (1,1)} |
Release 写、Acquire 读 | 30 | 同上 |
SeqCst 写、SeqCst 读 | 30 | 同上 |
Relaxed 读写,两边都在写读之间 fence(SeqCst) | 15 | {(0,1), (1,0), (1,1)} |
| 同上,只有一边有 fence | 30 | {(0,0), (0,1), (1,0), (1,1)} |
全 SeqCst 访问的 SB 在 C++ 里不允许 (0, 0),loom 给出它,这就是 README 说的 false alarm。用 fence 改写后 (0, 0) 消失,交错数也少一半;只有一边加 fence 不够。上游 tests/fence.rs:77-101 的 sb_fences 是同一个模型。
crossbeam 卷 Chase-Lev 双端队列 的 E5 正是这个形状:LIFO pop 与 steal 各一道 fence(SeqCst),去掉任一道,第一个交错就把同一个任务取出两次。那个模型里 SeqCst 不只出现在 fence 上,pop 与 steal 对 front 的 compare_exchange 成功序也是 SeqCst;按本章,这两处在 loom 里等同 AcqRel 的 RMW(RMW 本来就只读最新写)。完整协议通过、去掉任一道 fence 就失败,起区分作用的是 fence,那一页「loom 0.7.2 确实模拟了 SeqCst fence 的语义」的结论成立。
RWC:read-to-write causality
三个线程:主线程写 x = 1;线程 1 读 x 得 a、读 y 得 b;线程 2 写 y = 1、读 x 得 c。(a, b, c) = (1, 0, 0) 在两处都加 fence(SeqCst) 时被 C++20 禁止。目录 exp-loom/memory-model/tests/fences.rs 的 rwc_fences,上游对应 tests/fence.rs:136-175 的 rwc_syncs(注释:这个结果 C/C++11 允许,后来被认为太弱,C/C++20 禁止)。
| 写法 | 交错数 | 结果 |
|---|---|---|
全 Relaxed,没有 fence | 3 772 | 8 种组合全部出现 |
线程 1、线程 2 的两次访问之间各一道 fence(SeqCst) | 2 582 | 7 种,只少 (1, 0, 0) |
上游还有一个 W+RWC(tests/fence.rs:177-217):线程 1 不直接读 x,而是通过主线程对 z 的 Release 写与自己的 Acquire 读间接知道 x 已写,其余同 RWC,同样断言 (1, 0, 0) 不出现。三个上游用例(SB+fences、RWC+syncs、W+RWC)检验的都是 CDSChecker 图 9 那类「SC fence 限制读值」的效果,loom 用全局时钟实现,而不是逐条实现图 9 的规则。本卷没有单独重跑 W+RWC。
IRIW:independent reads of independent writes
四个线程:主线程写 x,线程 W 写 y;线程 R1 先读 x 再读 y,R2 先读 y 再读 x。非 SC 结果是「R1 看到 x 已写、y 未写,R2 看到 y 已写、x 未写」,即 (1, 0, 1, 0):两个读者对两次独立写的先后看法相反。全 SeqCst 访问时 C++ 禁止它。目录 exp-loom/memory-model/tests/fences.rs 的 iriw_*。
| 写法 | 交错数 | 出现 (1, 0, 1, 0) |
|---|---|---|
两次写、四次读都是 SeqCst | 1 180 969 | 是 |
全 Relaxed,两个读者的两次读之间各一道 fence(SeqCst) | 1 029 359 | 否 |
和 SB 一样:SeqCst 访问挡不住,fence 挡得住。四个线程加主线程,已经到了 MAX_THREADS;不设抢占上限时交错数上百万,是本部分最大的模型。
怎么用这些结论
- 用 SeqCst 访问写的、依赖全序的正确代码(SB、IRIW 形状),loom 可能报错。确认是这个原因后,两种处理:改写成「
Relaxed/Acquire/Release访问 +fence(SeqCst)」的形式(这往往也是 C++ 实现里实际起作用的东西),或者接受它是 loom 的限制、不为它写 loom 测试。 - 依赖
fetch_add(0)之类 RMW「读最新值」的写法,在 loom 里和 SeqCst 无关地成立(RMW 与释放序列)。 - 这些条目连同 load buffering 汇总在 07 SeqCst 与 load buffering。
小结
fence(Acquire)对所有已被看到的写补做 acquire;fence(Release)把当前时钟存进released,之后每次写都带上它;fence(Relaxed)panic;fence 不是调度点。fence(SeqCst)=AcqRel+ 与全局seq_cst_causality双向 join:后执行 SC fence 的线程得到先执行者的全部因果历史。- SeqCst 访问调用的
seq_cst()是空函数,只剩「SeqCst load 跳过旧 SeqCst 写」一条规则;Mutex、RwLock、Notify的seq_cst都因此不起作用。 - 历史:#108(2020-02)把全局 join 从 SeqCst 访问上拿掉,#220(2021-09)把同一段代码只用于
fence(SeqCst)。 - SB、RWC、IRIW 三个 litmus:SeqCst 访问挡不住非 SC 结果(假阳性方向),
fence(SeqCst)挡得住;只有一边加 fence 不够。