题目
使用多线程安全修改共享计数器
信息
- 类型:问答
- 难度:⭐
考点
线程创建,共享数据同步,Arc与Mutex使用
快速回答
修正代码的关键步骤:
- 使用
Arc<Mutex<T>>包装共享数据 - 在闭包内克隆Arc并移动所有权
- 通过
lock()获取互斥锁修改数据
正确代码结构:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..2 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
// 加锁修改数据
});
handles.push(handle);
}
// 等待线程结束
}
## 解析
问题背景
在Rust并发编程中,当多个线程需要修改同一共享数据时,必须解决两个核心问题:1) 所有权的跨线程传递;2) 数据竞争的避免。原始代码尝试直接在线程闭包中移动Mutex的所有权,这违反了Rust的所有权规则。
错误代码分析
// 错误示例
let counter = Mutex::new(0);
thread::spawn(move || {
// 错误:counter被第一个线程移动后,第二个线程无法使用
let mut num = counter.lock().unwrap();
*num += 1;
});主要错误:
- 所有权冲突:
move闭包尝试获取counter的所有权,但多个线程不能共享同一所有权 - 未实现共享:
Mutex本身不具备多线程共享能力,需要配合智能指针
解决方案
使用Arc<Mutex<T>>组合:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// 用Arc包装Mutex
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..2 {
// 克隆Arc指针(增加引用计数)
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
for _ in 0..1000 {
// 获取互斥锁
let mut num = counter.lock().unwrap();
*num += 1; // 安全修改数据
} // 锁自动释放
});
handles.push(handle);
}
// 等待所有线程完成
for handle in handles {
handle.join().unwrap();
}
// 打印最终结果
println!("Result: {}", *counter.lock().unwrap());
}核心原理
- Arc(原子引用计数):实现所有权的多线程共享,通过克隆增加引用计数
- Mutex(互斥锁):确保同时只有一个线程能访问数据,防止数据竞争
lock()方法阻塞线程直到获取锁- 返回的
MutexGuard在离开作用域时自动释放锁
最佳实践
- 最小化锁范围:在
{}代码块内持有锁,尽快释放 - 错误处理:使用
lock().expect("锁错误")处理可能的PoisonError - 性能考虑:对于高并发场景,可评估
RwLock(读写锁)替代
常见错误
| 错误类型 | 现象 | 解决方案 |
|---|---|---|
| 忘记克隆Arc | 编译错误:所有权已移动 | 每次循环内克隆Arc |
| 未处理锁错误 | 线程panic导致锁污染 | 使用unwrap或expect |
| 锁嵌套 | 死锁风险 | 避免在持有一个锁时获取另一个锁 |
扩展知识
- 原子类型:对于简单整数操作,
AtomicUsize比Mutex更高效 - 作用域线程:
std::thread::scope可避免显式使用Arc - 无锁编程:高阶主题,使用
crossbeam等库实现复杂并发结构