Execution 与对象仓库
主线位置:主线 1 的状态容器。一轮执行里发生的一切都记在一个
Execution里:走过的分支(Path)、每个线程的状态和时钟(thread::Set)、每个 loom 对象的状态(object::Store)。step把它换成下一轮的初始状态;schedule(下下页)读它来决定下一步跑谁。
flowchart LR
subgraph user["对外类型(sync / cell / thread …)"]
a["AtomicUsize"] --> ar["rt::Atomic { state: Ref<State> }"]
m["Mutex<T>"] --> mr["rt::Mutex { state: Ref }<br/>+ std::sync::Mutex<T> 存数据"]
c["UnsafeCell<T>"] --> cr["rt::Cell { state: Ref }<br/>+ std::cell::UnsafeCell<T>"]
end
subgraph ex["Execution(每轮一个)"]
path["path: Path(分支记录)"]
th["threads: thread::Set(状态、causality、dpor_vv、operation)"]
st["objects: object::Store = Vec<Entry>"]
other["raw_allocations / arc_objs / lazy_statics"]
end
ar -->|下标| st
mr -->|下标| st
cr -->|下标| st
Execution 的字段
| 字段 | 类型 | 管什么 | 本卷 |
|---|---|---|---|
id | Id | 这一轮的编号;线程 Id 里带着它,防止拿上一轮的线程号用到这一轮 | 本页 |
path | Path | 已做的和待做的分支选择 | Path |
threads | thread::Set | 每个线程的状态(可运行 / 阻塞 / 让出 / 结束)、三套向量时钟、下一步要做的 operation、线程局部变量 | DPOR、向量时钟与同步点 |
lazy_statics | lazy_static::Set | 这一轮已初始化的 lazy_static! 值 | 线程、线程局部与 lazy_static |
objects | object::Store | 所有 loom 对象的状态 | 本页 |
raw_allocations | HashMap<usize, Allocation> | loom::alloc::alloc 出来的指针 → 泄漏跟踪对象 | 泄漏检测 |
arc_objs | HashMap<*const (), std::sync::Arc<rt::Arc>> | loom::sync::Arc 的数据指针 → 计数对象 | Arc、mpsc 与 future |
max_threads | usize | 来自 Builder | model 与 Builder |
max_history | usize | 固定写 7,只存不用 | 见下 |
location、log | bool | 来自 Builder;log 在 0.7.2 里没有被读 | model 与 Builder |
max_history 在整个 crate 里只出现在这个文件的四处:声明、new 里赋值 7、step 里搬到新 Execution。真正限制每个原子保留几次写的是常量 MAX_ATOMIC_HISTORY = 7(rt/mod.rs:64-65),它决定了 State.stores 数组的长度,见 写历史与 coherence。
new、new_thread 与 step
Execution::new 只在 check 开头调一次。Path 和 object::Store 都按 max_branches(默认 1 000)预留容量。注释写明「The same instance is reused across permutations」。
new_thread 是 spawn 时登记新线程的地方:
pub(crate) fn new_thread(&mut self) -> thread::Id {
let thread_id = self.threads.new_thread();
let active_id = self.threads.active_id();
let (active, new) = self.threads.active2_mut(thread_id);
new.causality.join(&active.causality);
new.dpor_vv.join(&active.dpor_vv);
// Bump causality in order to ensure CausalCell accurately detects
// incorrect access when first action.
new.causality[thread_id] += 1;
active.causality[active_id] += 1;
thread_id
}
子线程继承父线程的两套时钟:causality(内存模型的 happens-before)和 dpor_vv(DPOR 的依赖关系)。所以「父线程 spawn 之前做的事」对子线程总是可见,也总被 DPOR 当作「在子线程之前」。然后父子两边的 causality 各加一,注释说是为了让单元格竞争检测能抓到子线程第一个动作就和父线程冲突的情况。两套时钟的分工见 DPOR。
step 在每轮结束时调用,返回下一轮的 Execution:
pub(crate) fn step(self) -> Option<Self> {
let id = Id::new();
// …把字段逐个搬出来…
if !path.step() {
return None;
}
objects.clear();
lazy_statics.reset();
raw_allocations.clear();
arc_objs.clear();
threads.clear(id);
Some(Execution { id, path, threads, objects, /* … */ })
}
- 先让
path.step()找下一条路径;找不到就None,整个模型结束。 - 其余容器
clear():保留已分配的容量,内容清空。下一轮所有对象从下标 0 重新插。 threads.clear(id)只留一个新的 0 号线程,seq_cst_causality也清零(rt/thread.rs:351-360)。
Id::new 从一个全局 AtomicUsize 取号,起点是 46 413 762(execution.rs:273-286),注释说这个数是随便挑的,「mostly to avoid collision with "zero" to aid with debugging」。每轮一个新号,thread::Id 里都带着它。
object::Store:所有对象都是一个下标
rt/object.rs 的核心只有两个类型:
pub(super) struct Store<T = Entry> {
/// Stored state for all objects.
entries: Vec<T>,
}
pub(super) struct Ref<T = ()> {
/// Index in the store
index: usize,
_p: PhantomData<T>,
}
Store 是一个 Vec,insert 就是 push 并返回下标(object.rs:165-177)。Ref<T> 是一个带类型标记的下标,Copy。Entry 由 objects! 宏生成(object.rs:73-143),9 个变体:
Entry 变体 | 状态类型 | 谁创建 |
|---|---|---|
Alloc | rt::alloc::State | loom::alloc::alloc、Track::new |
Arc | rt::arc::State | loom::sync::Arc::new |
Atomic | rt::atomic::State | 所有 Atomic*::new |
Mutex | rt::mutex::State | Mutex::new |
Condvar | rt::condvar::State | Condvar::new |
Notify | rt::notify::State | Notify::new、每次 thread::spawn(JoinHandle 用它)、future::block_on |
RwLock | rt::rwlock::State | RwLock::new |
Channel | rt::mpsc::State | mpsc::channel |
Cell | rt::cell::State | UnsafeCell::new、cell::Cell::new |
宏同时为每个状态类型实现 Object trait(get_ref / get_mut 按变体匹配),Ref<T>::get 拿到的就是 &T;类型对不上会 panic「[loom internal bug] unexpected object stored at reference」。
loom 原子的内存表示
一个 loom::sync::atomic::AtomicUsize 往下是三层壳:AtomicUsize(Atomic<usize>) → sync::atomic::Atomic { state: rt::Atomic<usize> } → rt::Atomic { state: object::Ref<State>, _p }(rt/atomic.rs:71-75)。值不在对象里,在 Execution.objects 的那一项里:最近 7 次写各自的值,统一存成 u64。
实测(exp-loom/runtime/tests/sizes.rs,size_of):
| 类型 | loom | std |
|---|---|---|
AtomicBool | 8 | 1 |
AtomicU64 | 8 | 8 |
AtomicPtr<u8> | 8 | 8 |
UnsafeCell<u8> | 16 | 1 |
Mutex<u8> | 24 | 16 |
Arc<u8> | 16 | 8 |
所有 loom 原子都是 8 字节,就是一个 usize 下标。UnsafeCell、Mutex 是「一个下标 + 一个 std 的同名类型存数据」(sync/mutex.rs:8-11 里的 data: std::sync::Mutex<T>)。由此有三个直接后果:
- 不能 transmute。 把
&AtomicUsize当&usize用、或者在AtomicCell这类「按底层类型的位模式操作」的代码里换成 loom 原子,都不成立。crossbeam 的AtomicCell在 loom 下不编译就是这个原因(crossbeam 卷:怎么验证并发代码)。 - 不能
const构造。new要往Execution.objects里插一项,必须在模型里调用;在模型外调用就是 协程调度器 里那条「cannot access Loom execution state from outside a Loom model」。 - 对象跨轮无效。 每轮开始
objects.clear(),下标从 0 重新分配。把一个 loom 对象藏在 std 的static或OnceLock里留到下一轮,它的下标指向的是另一个对象(或越界)。所以模型里的 loom 对象都要在闭包里新建。
每次操作:先登记 operation,再调度
loom 对象的每个「可能和别的线程冲突」的操作,在真正执行前都先调一次 branch(rt/mod.rs:116-135):
fn branch<F, R>(f: F) -> R
where
F: FnOnce(&mut Execution) -> R,
{
let (ret, switch) = execution(|execution| {
let ret = f(execution);
let switch = execution.schedule();
trace!(?switch, "branch");
(ret, switch)
});
if switch {
Scheduler::switch();
}
ret
}
f 负责把当前线程的 operation 字段设成「我下一步要对哪个对象做什么」,然后 schedule() 决定下一步谁跑;如果不是自己,就 switch() 让出协程。操作本身在 branch 返回之后才执行:被切走的线程把 operation 留在身上,等下次被选中、switch() 返回,才接着做。
operation 的类型是 Operation:
pub(super) struct Operation {
obj: Ref,
action: Action,
location: Location,
}
pub(super) enum Action {
Arc(rt::arc::Action), // RefInc / RefDec / Inspect
Atomic(rt::atomic::Action), // Load / Store / Rmw
Channel(rt::mpsc::Action), // MsgSend / MsgRecv
RwLock(rt::rwlock::Action), // Read / Write
/// Generic action with no specialized dependencies on access.
Opaque,
}
Action 的作用是告诉 DPOR「这次操作和同一对象上哪些早先的访问相关」。原子、Arc、channel 按动作区分(例如 load 只和上一次非 load 相关);Mutex、Condvar、Notify、RwLock 的 last_dependent_access() 不看动作,一律取「上一次访问」(object.rs:211-225),所以它们用 Opaque 也没有损失。RwLock 的 Read / Write 不参与依赖判断,只用于 rwlock.rs 自己:拿到读锁后,把所有「下一步要对这把锁做 Write」的线程标成阻塞(rt/rwlock.rs:204-218)。具体的依赖规则在 DPOR。
Ref 上有四个包装 branch 的方法(object.rs:330-378):
| 方法 | 做什么 | 谁在用 |
|---|---|---|
branch_action(action) | 登记 operation,调度 | 原子(rt/atomic.rs:389)、Arc、mpsc::send、RwLock 的 try_read / try_write |
branch_opaque() | branch_action(Opaque) | Mutex::try_acquire_lock、Condvar 的 wait / notify_*、Notify::notify 与不必等待的 wait |
branch_acquire(is_locked) | 登记 Opaque;若 is_locked,把当前线程标成阻塞,再调度 | Mutex::acquire_lock、必须等待的 Notify::wait(源码注释「This should become branch_disable」) |
branch_disable(action, disable) | 同上,但带具体动作 | RwLock 的读锁 / 写锁获取、mpsc::recv(channel 空时阻塞) |
阻塞的写法值得注意:branch_acquire 先把自己标成 Blocked,再 schedule()。schedule 看到当前线程不可运行,必然换线程;等持锁方 release_lock 把它改回可运行、某次调度又选中它,switch() 才返回,acquire_lock 接着执行 post_acquire(rt/mutex.rs:44-47)。没有任何线程能把它改回来,就是 死锁。
不调 branch 的操作也有:UnsafeCell::with / with_mut、原子的 unsync_load / with_mut、thread::spawn、Mutex 的解锁。它们只改状态、做检查,不产生调度分支。
Path 也存在一个 Store 里
Path 的分支列表 branches 的类型是 object::Store<path::Entry>(path.rs:21-24),path::Entry 由同一个 objects! 宏生成,三个变体 Schedule、Load、Spurious。Store 多了一个 truncate,Path::step 用它从末尾截断。所以「路径上的第 k 个分支」和「对象仓库里的第 k 个对象」是同一种下标,只是两个不同的 Vec。DPOR 记录的「上一次访问发生在路径的哪个位置」(Access.path_id)就是 branches 的下标。
小结
Execution装一轮的全部状态:path、threads、objects和三张辅助表;只在check开头建一次,step复用容器、换新Id(从 46 413 762 起)。max_history字段只存不用,真正的 7 是MAX_ATOMIC_HISTORY。new_thread让子线程继承父线程的causality和dpor_vv,两边causality各加一。- 所有 loom 对象都是
object::Store(一个Vec<Entry>)里的一项,对外类型只握一个 8 字节下标:loom 原子不能 transmute、不能const构造、不能跨轮保存。 - 每个可能冲突的操作先
branch:登记operation、schedule()、必要时switch();操作在被选中后才执行。branch_acquire/branch_disable先把自己标成阻塞再调度。