同步原语的模型
02 运行时 讲了 loom 怎么调度、怎么回溯,03 内存模型 讲了原子变量的写历史与 happens-before。这一部分讲剩下的对外类型:锁、条件变量、线程、静态变量、引用计数、channel、future。它们的形状几乎一样:
flowchart LR
subgraph pub["对外类型(src/sync、src/thread.rs ...)"]
m["loom::sync::Mutex<T>"]
a["loom::sync::Arc<T>"]
ch["mpsc::Sender / Receiver"]
end
subgraph rt["rt 运行时对象(只有状态,没有数据)"]
rm["rt::Mutex:持有者、Synchronize"]
ra["rt::Arc:ref_cnt、三类上次访问"]
rc["rt::Channel:消息数、每条消息一个 Synchronize"]
end
subgraph std["真正存数据的 std 容器"]
sm["std::sync::Mutex<T>"]
sa["std::sync::Arc<T>"]
sc["std::sync::mpsc"]
end
m --> rm
m --> sm
a --> ra
a --> sa
ch --> rc
ch --> sc
- rt 对象放在
Execution的对象仓库里(Execution 与对象仓库),只记录调度和内存模型需要的状态:谁持有锁、计数是多少、上一次相关访问在路径的哪个位置、一个或多个Synchronize(向量时钟与同步点)。 - 数据放在 std 的同名容器里。loom 用 rt 对象决定「这一步谁能走」,走到了再去拿 std 容器;所以 std 容器那边永远不会真的竞争,它的
lock()、try_read()在 loom 里只起「把数据借出来」的作用。 - 每个对外操作先调一次
branch_*(调度点),再改 rt 状态、做 acquire / release 同步。阻塞的写法是把当前线程标成Blocked,由Execution::schedule去挑别的线程;释放方把等在同一对象上的线程改回可运行。
四章:
- Mutex 与 RwLock:
branch_acquire与「加锁成功后把其他想拿锁的线程标成阻塞」;解锁不是调度点;不毒化;RwLock的读者集合、没有写者优先、同线程嵌套读的内部错误(E14 与反向验证)。 - Condvar、Notify 与 park:
Condvar没有虚假唤醒,if写法也通过(E9);wait_timeout不超时;loom 独有的Notify每个对象最多伪唤醒一次,同样的if写法换成Notify第 5 个交错就被抓到(E9 反向);rt::park消费预存的 unpark。 - 线程、线程局部与 lazy_static:
spawn没有Send约束;join恒为Ok,子线程 panic 直接让模型失败(E15,std 对照);最多 5 个线程(E12);thread_local!每线程一份;lazy_static!每轮重新初始化,一轮里可能初始化两次(E13 与反向验证),没人 join 的线程碰它会报错。 - Arc、mpsc 与 future:
Arc的三种动作与依赖关系;mpsc 每条消息一个同步点;发送方全部 drop 后recv被报成死锁、接收方 drop 后send被报成消息泄漏;recv_timeout与Barrier是调用即 panic 的桩;block_on与AtomicWaker抓丢唤醒(E18)。
和 std 的语义差别一览
每一项都标出方向:假阳性是 loom 报错而代码在真实环境里没问题;假阴性是代码有问题而 loom 通过。汇总见 07 能力边界。
| 原语 | loom 0.7.2 的行为 | std 的行为 | 方向 |
|---|---|---|---|
Mutex | 不毒化;lock 恒 Ok | 持锁线程 panic 后毒化 | 测不到毒化分支 |
RwLock | 读者不挡读者,有写者在等时新读者照样进 | 平台相关,文档不保证 | 假阴性(写者饥饿、嵌套读死锁测不到) |
RwLock 同线程嵌套读 | 报「invalid internal loom state」后 abort | 可能死锁,可能成功 | 假阳性 |
Condvar::wait | 没有虚假唤醒 | 可能虚假唤醒 | 假阴性 |
Condvar::wait_timeout | 永不超时 | 超时返回 | 假阳性(被报成死锁) |
thread::park | 没有虚假返回 | 可能虚假返回 | 假阴性 |
JoinHandle::join | 恒 Ok,子线程 panic 让模型失败 | 返回 Err | 假阳性 |
thread::spawn | 没有 Send 约束 | 要求 Send | 编译期差别 |
lazy_static! | 每轮重新初始化,一轮内可能初始化两次 | 只初始化一次 | 与真实行为不同 |
mpsc::Receiver::recv | 发送方全部 drop 后阻塞,被报成死锁 | 返回 Err(RecvError) | 假阳性 |
mpsc::Sender::send | 接收方 drop 后计入消息,报「Messages leaked.」 | 返回 Err(SendError) | 假阳性 |
recv_timeout、Barrier | 调用即 unimplemented! | 正常 | 不可用 |
本部分的实验
所有实验都用 RUSTFLAGS="--cfg loom" cargo test --release --offline、单独的 target 目录;会 abort 的场景每个单独一个 #[test],逐个 --exact 运行。交错数是确定的,与机器负载无关。
| # | 目录 | 看什么 | 章 |
|---|---|---|---|
| E9 | exp-loom/survey/tests/misc.rs、exp-loom/primitives/tests/prim.rs | Condvar 用 if 等待通过;换成 Notify 被抓到;wait_timeout 被报成死锁 | Condvar、Notify 与 park |
| E10 | exp-loom/survey/tests/misc.rs | 同线程两次加锁报死锁 | Mutex 与 RwLock |
| E12 | exp-loom/survey/tests/misc.rs | 主线程外第 5 个线程触发断言 | 线程、线程局部与 lazy_static |
| E13 | 同上两处 | lazy_static! 初始化次数;thread_local! 析构次数 | 线程、线程局部与 lazy_static |
| E14 | 同上两处 | 同线程嵌套读的内部错误;不嵌套时通过 | Mutex 与 RwLock |
| E15 | 同上两处 | 子线程 panic:loom 下模型失败,std 下 join 返回 Err | 线程、线程局部与 lazy_static |
| E18 | exp-loom/primitives/tests/prim.rs | block_on + AtomicWaker:先注册后检查通过,先检查后注册报死锁 | Arc、mpsc 与 future |
| — | exp-loom/primitives/tests/prim.rs | Arc 与原子的交错数对照、strong_count 的取值、mpsc 的断开与泄漏、桩的报错原文 | Arc、mpsc 与 future |
💬 关联讨论
发起讨论加载中...