1. 为什么要共享 Mutex<T>?
想象你和几个朋友一起用一台公共电脑:
- 问题:如果大家同时用键盘鼠标,电脑会乱套
- 解决方案:给电脑配个"使用牌",谁拿到牌子谁用
在 Rust 中:
- 公共电脑 = 需要共享的数据
- 使用牌 =
Mutex<T> - 朋友们 = 多个线程
2. 如何共享 Mutex<T>?
2.1 基本步骤
- 创建需要保护的数据
- 用 Mutex 包装它
- 用 Arc(原子引用计数)包装 Mutex
- 克隆 Arc 给每个线程
2.2 具体代码
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// 1. 创建需要共享的数据(这里是个计数器)
let counter = Arc::new(Mutex::new(0)); // 包装两层
let mut handles = vec![]; // 保存线程句柄
for _ in 0..10 {
// 2. 克隆Arc(增加引用计数)
let counter = Arc::clone(&counter);
// 3. 创建线程
let handle = thread::spawn(move || {
// 4. 获取锁
let mut num = counter.lock().unwrap();
// 5. 修改数据
*num += 1;
// 6. 锁会自动释放(当num离开作用域)
});
handles.push(handle);
}
// 等待所有线程完成
for handle in handles {
handle.join().unwrap();
}
// 查看最终结果
println!("最终计数: {}", *counter.lock().unwrap());
// 正确输出: 10(因为有10个线程各+1)
}
3. 为什么需要这样设计?
3.1 为什么用 Arc?
Arc= Atomic Reference Count(原子引用计数)- 就像"使用牌"的登记本,记录有多少人想用电脑
- 每个线程拿到自己的"登记副本"(克隆Arc)
- 当所有线程用完,登记本归零,数据自动清理
3.2 为什么用 Mutex?
- 确保同一时间只有一个线程能修改数据
- 防止"数据竞争"(多个线程同时写导致数据错乱)
4. 实际应用场景
场景1:银行账户系统
struct BankAccount {
balance: f64,
owner: String,
}
let account = Arc::new(Mutex::new(BankAccount {
balance: 1000.0,
owner: "张三".to_string(),
}));
// 多个ATM线程同时操作账户
for _ in 0..5 {
let acc = Arc::clone(&account);
thread::spawn(move || {
let mut acc = acc.lock().unwrap();
acc.balance -= 100.0; // 取款100元
});
}
场景2:游戏服务器
let player_positions = Arc::new(Mutex::new(HashMap::new()));
// 每个玩家一个线程更新位置
for player_id in 0..10 {
let positions = Arc::clone(&player_positions);
thread::spawn(move || {
let mut pos = positions.lock().unwrap();
pos.insert(player_id, (rand::random(), rand::random()));
});
}
5. 工作原理图解
主线程
│
├─ 创建 Arc<Mutex<T>>
│ (引用计数=1)
│
├─ 克隆Arc → 线程1 (引用计数=2)
│ ├─ lock() 获取锁
│ ├─ 修改数据
│ └─ 自动释放锁
│
├─ 克隆Arc → 线程2 (引用计数=3)
│ ├─ 尝试lock()
│ ├─ 等待线程1释放
│ ├─ 获取锁
│ └─ 修改数据
│
└─ 等待所有线程...
最后引用计数归零,自动清理
6. 常见问题解答
Q: 为什么不直接用 Mutex 而要套 Arc? A: 因为:
- Mutex 本身不能安全地跨线程共享所有权
- Arc 负责线程安全的引用计数
- Mutex 负责线程安全的数据访问
Q: 可以共享没有 Mutex 的 Arc<T> 吗?
A: 可以,但只能用于只读数据。要修改数据必须加 Mutex 或 RwLock。
Q: 为什么 lock() 返回的是 LockResult? A: 因为:
- 可能获取锁失败(比如持有锁的线程 panic 了)
- 需要使用
unwrap()或?处理可能的错误
Q: 这样会影响性能吗? A: 会有一定开销:
- Arc 的克隆/销毁有原子操作开销
- Mutex 的 lock/unlock 有系统调用开销
- 但在合理使用下(不频繁争抢锁)开销很小
7. 实用技巧
7.1 缩小锁范围
// 不好:锁住整个函数
let data = lock.lock().unwrap();
long_running_operation(&data);
// 好:只锁必要部分
let value = {
let data = lock.lock().unwrap();
data.clone() // 复制数据到锁外
};
long_running_operation(&value);
7.2 避免死锁
// 危险:嵌套获取多个锁
let lock1 = Arc::new(Mutex::new(0));
let lock2 = Arc::new(Mutex::new(0));
// 线程A
let _a = lock1.lock().unwrap();
let _b = lock2.lock().unwrap(); // 如果线程B以相反顺序获取,会死锁
// 解决方案:总是按固定顺序获取锁
7.3 使用 try_lock
match lock.try_lock() {
Ok(guard) => {
// 成功获取锁
}
Err(_) => {
// 锁被占用,做其他事情
}
}
8. 总结
共享 Mutex<T> 的要点:
- Arc 是快递员:安全地把数据"快递"给各个线程
- Mutex 是门卫:确保每次只有一个人能修改数据
- 配合使用:
Arc<Mutex<T>>是最常见的线程安全共享模式
使用场景:
- 任何需要多个线程修改同一数据的场合
- 特别是写操作频繁的场景(相比 RwLock)
线程安全共享 = Arc + Mutex/RwLock + 合理的作用域控制