Rust in Everything
基于 loom 0.7.2

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&lt;T&gt;"] --> mr["rt::Mutex { state: Ref }<br/>+ std::sync::Mutex&lt;T&gt; 存数据"]
    c["UnsafeCell&lt;T&gt;"] --> cr["rt::Cell { state: Ref }<br/>+ std::cell::UnsafeCell&lt;T&gt;"]
  end
  subgraph ex["Execution(每轮一个)"]
    path["path: Path(分支记录)"]
    th["threads: thread::Set(状态、causality、dpor_vv、operation)"]
    st["objects: object::Store = Vec&lt;Entry&gt;"]
    other["raw_allocations / arc_objs / lazy_statics"]
  end
  ar -->|下标| st
  mr -->|下标| st
  cr -->|下标| st

Execution 的字段

rt/execution.rs:10-39:

字段类型管什么本卷
idId这一轮的编号;线程 Id 里带着它,防止拿上一轮的线程号用到这一轮本页
pathPath已做的和待做的分支选择Path
threadsthread::Set每个线程的状态(可运行 / 阻塞 / 让出 / 结束)、三套向量时钟、下一步要做的 operation、线程局部变量DPOR、向量时钟与同步点
lazy_staticslazy_static::Set这一轮已初始化的 lazy_static! 值线程、线程局部与 lazy_static
objectsobject::Store所有 loom 对象的状态本页
raw_allocationsHashMap<usize, Allocation>loom::alloc::alloc 出来的指针 → 泄漏跟踪对象泄漏检测
arc_objsHashMap<*const (), std::sync::Arc<rt::Arc>>loom::sync::Arc 的数据指针 → 计数对象Arc、mpsc 与 future
max_threadsusize来自 Buildermodel 与 Builder
max_historyusize固定写 7,只存不用见下
location、logbool来自 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 变体状态类型谁创建
Allocrt::alloc::Stateloom::alloc::alloc、Track::new
Arcrt::arc::Stateloom::sync::Arc::new
Atomicrt::atomic::State所有 Atomic*::new
Mutexrt::mutex::StateMutex::new
Condvarrt::condvar::StateCondvar::new
Notifyrt::notify::StateNotify::new、每次 thread::spawn(JoinHandle 用它)、future::block_on
RwLockrt::rwlock::StateRwLock::new
Channelrt::mpsc::Statempsc::channel
Cellrt::cell::StateUnsafeCell::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):

类型loomstd
AtomicBool81
AtomicU6488
AtomicPtr<u8>88
UnsafeCell<u8>161
Mutex<u8>2416
Arc<u8>168

所有 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 先把自己标成阻塞再调度。

💬 关联讨论

发起讨论
加载中...