Rust 共享状态并发
Mutex/RwLock 与 Arc 组合实现共享可变状态;锁毒化、粒度与死锁风险。
#concept
#growing
#dev
#rust
[!info] related notes
Rust 共享状态并发
这个概念为什么出现
有时多线程必须共享可变状态(计数器、缓存)。
Rust 用 Mutex<T>/RwLock<T> 保护数据,用 Arc 共享锁本身。
[!abstract] 一句话理解
Arc<Mutex<T>>让多线程共享同一把锁与数据;只有持有 guard 时才能访问T,把互斥编码进类型。
最小可运行示例
场景:限流计数器——多线程递增通过数
示例并发递增“已放行请求”计数,并未实现窗口、额度判定或拒绝请求的完整限流逻辑。
use std::sync::{Arc, Mutex};
use std::thread;
// 业务意图:并发安全递增计数器。
// 教学点:Arc 共享;lock 获取 guard;自动 unlock(Drop)。
fn main() {
let counter = Arc::new(Mutex::new(0u64));
let mut handles = vec![];
for _ in 0..8 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut guard = counter.lock().unwrap();
*guard += 1;
}));
}
for h in handles { h.join().unwrap(); }
println!("allowed={}", *counter.lock().unwrap());
}
建议运行:cargo run
期望输出:
allowed=8
结合场景再看三个关注点
- guard 离开作用域解锁(RAII)
- 锁粒度:只包最小临界区
- std 锁的 poison 是建议性信号:持有 Mutex 时发生 panic 通常会标记毒化;它不保证捕获所有不变量破坏
核心概念与准确模型
| 原语 | 场景 |
|---|---|
Mutex | 独占读写 |
RwLock | 多读或一写 |
Condvar | 等待条件 |
Barrier | 阶段同步 |
lock 返回 LockResult<MutexGuard<T>>。
设计动机
- 数据与锁绑定,避免未加锁访问
- 与 Send/Sync 协作
- Drop 防忘记解锁
边界与误区
- 死锁:锁顺序不一致
- 临界区太大损吞吐
- poison 后是否恢复需检查/修复数据不变量;into_inner 只是取出 guard,不自动修复数据。std RwLock 通常仅写锁期间的 panic 导致 poison
- async 中可以用 std Mutex 保护短小、无 await 的低争用临界区;持锁等待 I/O 或跨 await 易阻塞执行器或死锁,需要时选 async 锁并控制临界区
- Mutex 不保证重入,同线程重复加锁也可能死锁;RwLock 的调度与写者优先级不保证跨平台一致
[!warning] 常见误区:先 channel 能解决还硬上全局大锁 优先缩小共享面。
工程实践
- 优先消息传递,共享状态次之
- 固定加锁顺序
- 处理 poison 或使用更稳健策略
- 指标监控锁等待
本节总结
- Arc+Mutex 共享可变
- RAII 解锁
- 粒度与死锁是工程问题
自测题
- 为什么需要 Arc 包 Mutex?
- RwLock 读多写少的收益?
参考答案
- 示例中的线程需要共同持有锁,因此用 Arc;作用域线程也可借用同一 Mutex,不是所有共享锁都必须包 Arc。
- 允许多个读 guard 并存;实际收益取决于锁成本、争用与写者调度,读多不保证一定更快。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| The Book — Shared State | 官方书 | Mutex |
| std::sync::Mutex | 标准库 | API |