题目
实现一个线程安全的计数器
信息
- 类型:问答
- 难度:⭐⭐
考点
并发编程,原子操作,互斥锁,性能考虑
快速回答
实现线程安全计数器的两种主要方法:
- 原子操作:使用
std::sync::atomic::AtomicUsize实现无锁计数器 - 互斥锁:使用
std::sync::Mutex保护共享的usize值
关键选择依据:
- 高并发场景优先选择原子操作(性能更优)
- 需要复杂操作时使用互斥锁
问题背景
在多线程环境中,多个线程同时修改共享计数器会导致数据竞争(Data Race)。Rust的所有权系统可以防止部分并发错误,但共享可变状态仍需显式同步。
解决方案
1. 原子操作实现(推荐)
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
struct AtomicCounter {
count: AtomicUsize,
}
impl AtomicCounter {
pub fn new() -> Self {
AtomicCounter { count: AtomicUsize::new(0) }
}
pub fn increment(&self) {
self.count.fetch_add(1, Ordering::SeqCst);
}
pub fn get(&self) -> usize {
self.count.load(Ordering::SeqCst)
}
}
fn main() {
let counter = Arc::new(AtomicCounter::new());
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
counter.increment();
}
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Final count: {}", counter.get()); // 正确输出 10000
}原理说明:
AtomicUsize提供硬件级别的原子操作fetch_add保证递增操作的原子性Ordering::SeqCst确保全局内存顺序一致性
2. 互斥锁实现
use std::sync::{Arc, Mutex};
use std::thread;
struct MutexCounter {
count: Mutex<usize>,
}
impl MutexCounter {
pub fn new() -> Self {
MutexCounter { count: Mutex::new(0) }
}
pub fn increment(&self) {
let mut num = self.count.lock().unwrap();
*num += 1;
}
pub fn get(&self) -> usize {
*self.count.lock().unwrap()
}
}
// 使用示例与原子版本相同原理说明:
Mutex通过锁机制保证独占访问- 获取锁时可能阻塞线程
- 自动释放锁(通过
Droptrait)
最佳实践
- 优先选择原子操作:在简单计数器场景下,原子操作比互斥锁快5-10倍
- 内存顺序选择:
- 大多数场景使用
Ordering::Relaxed(仅需原子性) - 需要严格顺序时使用
Ordering::SeqCst
- 大多数场景使用
- 避免锁嵌套:互斥锁方案中不要在多处调用
lock()(易死锁)
常见错误
- 忘记同步:直接使用
usize导致未定义行为 - 锁中毒:线程panic时未处理
PoisonError - 内存顺序错误:过度使用
Ordering::SeqCst影响性能 - 原子操作误用:
load/store与fetch_add混用导致逻辑错误
性能对比
| 方法 | 10线程×100k操作 | 特点 |
|---|---|---|
| 原子操作 | ~15ms | 无阻塞,CPU缓存友好 |
| 互斥锁 | ~120ms | 线程阻塞,上下文切换开销 |
扩展知识
- 无锁编程:原子操作是实现无锁数据结构的基础
- 内存顺序:
Relaxed:仅保证原子性Release/Acquire:建立线程间happens-before关系
- 替代方案:
RwLock:适合读多写少场景crossbeam:提供更高效的无锁数据结构