Rust Arc/Mutex/RwLock:跨线程共享与可变性
更新时间:2026-09-20。多线程要同时拥有并修改一份数据时,Rust 的标准答案是
Arc<Mutex<T>>。为什么必须这两个搭着用,见 Send/Sync;单线程内部可变性用Rc/RefCell,见 Rc/RefCell。
一、各自解决什么问题
| 类型 | 解决 | 计数/策略 | 能否跨线程 |
|---|---|---|---|
Rc<T> | 单线程多所有者 | 非原子计数 | ❌ 非 Send/Sync |
Arc<T> | 多线程多所有者 | 原子计数 | ✅ Send/Sync(T 满足时) |
Mutex<T> | 互斥可变访问 | 同一时刻一个 &mut | ✅ Sync |
RwLock<T> | 读多写少 | 多个读锁共享、一个写锁独占 | ✅ Sync |
原子 AtomicUsize 等 | 无锁单个值 | CAS/原子指令 | ✅ |
二、Arc + Mutex 共享计数器
rust
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter); // 只克隆引用,计数+1
handles.push(thread::spawn(move || {
let mut g = counter.lock().unwrap(); // 拿 MutexGuard(即 &mut i32)
*g += 1;
})); // g 离开作用域自动解锁
}
for h in handles { h.join().unwrap(); }
assert_eq!(*counter.lock().unwrap(), 10);Arc::clone不复制数据,只把原子计数 +1;最后一个持有者 drop 时释放。lock()返回LockResult<MutexGuard<T>>,守卫离开作用域自动解锁(RAII),没有手动 unlock 也不易忘记释放。- Arc 给共享所有权,Mutex 给内部可变性,缺一不可。
三、RwLock:读多写少
rust
use std::sync::RwLock;
let data = RwLock::new(vec![1, 2, 3]);
let r1 = data.read().unwrap(); // 读锁,可同时多个
let r2 = data.read().unwrap();
// let w = data.write().unwrap(); // 有读锁时写锁阻塞,防死锁别在这里同时持有
let w = data.write().unwrap(); // 写锁独占(需 r1/r2 先释放)读操作远多于写、且读临界区有一定开销时,RwLock 比 Mutex 允许更高读并发;但读锁本身有簿记成本,临界区极短未必划算。
四、try_lock 与锁中毒
try_lock():拿不到锁立即返回Err(TryLockError::WouldBlock),可做非阻塞/带超时的兜底,避免硬等死锁。- 中毒(poison):持有锁的线程 panic 后,该 Mutex/RwLock 标记为中毒,之后
lock返回Err(PoisonError)——提醒你内部数据可能处于不一致状态。多数代码.unwrap()(信任一致性)或.into_inner()强行取回。
五、常见坑
- 用
Rc<RefCell>跨线程:编译就被 Send/Sync 拦下,换 Arc/Mutex。 - Arc 不加锁直接改:Arc 只给共享只读,要改必须内部可变(Mutex/原子)。
- 守卫作用域过大:
let g = lock();后做了 IO/长计算,锁持有过久拉低并发;缩短临界区,需要的值及时 clone 出。 - 多把锁获取顺序不一致:经典死锁,全局固定加锁顺序,或合并成一把锁。
- 持有读锁再申请写锁:RwLock 非重入,自死锁;注意守卫生命周期。
- 简单计数器上大锁:单个数字优先
AtomicUsize无锁,别无脑 Mutex。
总结
Arc 用原子引用计数让多个线程共有同一份数据(Rc 因非原子计数不能跨线程),但它只提供共享只读,要在多线程下修改就得靠 Mutex(互斥)、RwLock(读共享写独占)或 Atomic 原子类型提供内部可变性,标准写法是 Arc<Mutex<T>>;lock 返回的守卫靠 Drop 作用域自动解锁,try_lock 可非阻塞、持锁线程 panic 会令锁中毒,实践中要缩短临界区、固定多锁顺序防死锁、单值计数优先用无锁原子类型。