8.3 Mutex共享状态与Send、Sync


8.3 Mutex 共享状态与 Send、Sync

本节摘要:真正需要多线程读写同一数据时,Mutex 保证"一次一把锁",且解锁由守卫的 Drop 自动执行——忘解锁编不过或不可能。Send 与 Sync 两个标记 Trait 是跨线程的资格审查。读完你能用 Arc 加 Mutex 写标准共享状态,并解释 Rc 与 RefCell 为何不能跨线程。

共享状态的判例

use std::sync::{Arc, Mutex}; use std::thread; let counter = Arc::new(Mutex::new(0u32)); let mut handles = Vec::new(); for _ in 0..4 { let counter = Arc::clone(&counter); // 引用计数+1,原子操作 handles.push(thread::spawn(move || { let mut num = counter.lock().unwrap(); // 拿锁,得到守卫 *num += 1; })); // 守卫离开作用域,自动解锁 } for h in handles { h.join().unwrap(); } println!("计数:{}", *counter.lock().unwrap()); // 4,无竞争

三重防护:Arc 的克隆是原子计数(跨线程安全的多所有权);Mutex::lock 返回守卫,守卫实现了 Deref 与 Drop——通过守卫访问数据、守卫死亡即解锁;借用检查器再补一刀:同一线程持锁期间再 lock 会被借用规则驳回(E0382 之外又一层)。

Send 与 Sync:资格审查

标记 含义 反例
Send 类型可以移交给别的线程 Rc(非原子计数)
Sync 类型可以被多线程同时引用 RefCell(非同步的运行期借用检查)

Rc 与 RefCell 恰好是单线程版的 Arc 与 Mutex——一一对应、各有辖区。把 Rc 塞进线程,编译器报"Rc 不可 Send"(E0277),事故消灭在编译期。绝大多数类型自动获得这两个标记(编译器按字段推导),手动实现属于 Unsafe 领域,第 10 章边缘涉猎。

两对镜像的记忆框架

要点回顾

  • Arc 加 Mutex 是共享状态的标准配方,clone 计数、lock 访问;
  • 守卫即锁的化身,Drop 自动解锁,忘解锁不可能发生;
  • Send 管移交、Sync 管共享,Rc 与 RefCell 因它们止步单线程;
  • 能用通道就别共享——消息传递的产权清晰度永远高于锁。

E0277:Rc 跨线程的驳回书

把单线程的 Rc<RefCell<T>> 直接送上线程,是并发庭的经典第一案。报错原文值得整段读一遍。

use std::rc::Rc; use std::thread; fn main() { let counter = Rc::new(std::cell::RefCell::new(0u32)); let hs: Vec<_> = (0..4).map(|_| { let c = Rc::clone(&counter); thread::spawn(move || { // E0277 在此开庭 *c.borrow_mut() += 1; }) }).collect(); for h in hs { h.join().unwrap(); } println!("{}", counter.borrow()); }
error[E0277]: `Rc<RefCell<u32>>` cannot be sent between threads safely | = help: within `{closure}`, the trait `Send` is not implemented for `Rc<RefCell<u32>>` = note: required because it appears within the type `{closure}`

驳回理由不是"编译器保守",而是实打实的竞态:Rc 的计数更新非原子,两个线程同时 clone 会算错计数,造成双重释放。修法是法定换装:Rc 换 Arc、RefCell 换 Mutex,语义完全对应。

Arc 与 Mutex 的合租协议

use std::sync::{Arc, Mutex}; use std::thread; fn main() { let counter = Arc::new(Mutex::new(0u32)); let hs: Vec<_> = (0..4).map(|_| { let c = Arc::clone(&counter); thread::spawn(move || { let mut guard = c.lock().expect("锁未中毒"); *guard += 1; }) // guard 离开作用域自动解锁 }).collect(); for h in hs { h.join().unwrap(); } println!("合计 {}", *counter.lock().unwrap()); // 4 }

关键纪律:锁的粒度是 guard 的作用域,让临界区肉眼可见的最直接手法是用块 { let g = m.lock()...; ... } 限定,而不是把 guard 撒满整个函数。持有锁期间做 IO 或再取另一把锁,是下一桩案件的直接案因。

死锁:两把锁的交叉质证

use std::sync::{Arc, Mutex}; use std::thread; fn main() { let a = Arc::new(Mutex::new("档案A")); let b = Arc::new(Mutex::new("档案B")); let (a1, b1) = (Arc::clone(&a), Arc::clone(&b)); let (a2, b2) = (Arc::clone(&a), Arc::clone(&b)); let t1 = thread::spawn(move || { let _g1 = a1.lock().unwrap(); let _g2 = b1.lock().unwrap(); // 等待 b }); let t2 = thread::spawn(move || { let _g1 = b2.lock().unwrap(); let _g2 = a2.lock().unwrap(); // 等待 a:环形成,双双停摆 }); t1.join().unwrap(); // 永不返回 t2.join().unwrap(); }

预防三板斧:全程序规定锁的全序(永远按同一顺序取);缩小持有窗口,取用即还;多资源一次原子获取(锁包裹或合并为单一结构体状态)。死锁不 panic 不报错,只挂起,症状是线程卡死而 CPU 空闲——这条鉴别特征写进运维手册。

Send 与 Sync 的判词表

trait 承诺 典型实现者/否决者
Send 值可移交另一线程 大多数类型;否决者 Rc、RefCell
Sync &T 可跨线程共享 Arc、Mutex;否决者 Cell、RefCell

组合判定有两条速算:T: Sync 等价于 &T: Send;裸指针两者皆否,unsafe 实现可推翻。auto trait 意味着编译器按字段自动推导,禁止或强制用 unsafe impl 表态——第 10 章 Unsafe 一节接手这个话题。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U