外观
第 3 节:锁竞争——排队等锁最伤性能
症状
多线程程序(第 15 章)加了一堆 Mutex,线程越多反而越慢,甚至比单线程还慢。
诊断
Mutex 的规矩(第 15 章):同一时刻只有一个线程能进。当多个线程频繁争抢同一把锁,它们大部分时间在排队——这就是锁竞争(lock contention)。
更糟的是把重活干在锁里:
rust
// 慢:锁里干了 5 秒的活,所有线程排队 5 秒
let mut data = data.lock().unwrap();
for item in data.iter_mut() {
expensive_work(item); // 每个都花很久
}或者锁的粒度太粗:一次锁住一大片数据,本来能并行的部分全串行了。
解药
解药一:锁里只做"最小的事"——算完再进锁写结果:
rust
// 慢:在锁里做重活
let mut total = counter.lock().unwrap();
for i in &items {
*total += expensive_calc(i); // 重活在锁里,别人全等
}
// 快:重活在锁外,锁里只写一行
let sum: u64 = items.iter().map(expensive_calc).sum(); // 锁外并行算
let mut total = counter.lock().unwrap();
*total += sum; // 锁里只加一下解药二:能传消息就别共享(第 15 章哲学)——channel 比锁更适合"生产→消费":
rust
// 锁:每个线程抢同一个计数器
// channel:每个线程发消息,主线程汇总——没人排队解药三:换个"更轻的锁"——场景合适时:
| 工具 | 特点 | 场景 |
|---|---|---|
Mutex<T> | 通用,可改任意数据 | 复杂共享结构 |
RwLock<T> | 读多写少时并发读 | 配置表、缓存 |
AtomicUsize 等 | 无锁,一步到位 | 计数器、标志位(第 20 章访问计数器) |
| 不共享 | 各算各的,最后合并 | 并行求和(第 15 章蚂蚁) |
rust
// 计数器:AtomicUsize 无锁,比 Mutex 快得多
use std::sync::atomic::{AtomicUsize, Ordering};
let counter = AtomicUsize::new(0);
counter.fetch_add(1, Ordering::Relaxed); // 没有锁,没有排队预防
- 锁的三大纪律:锁要小(粒度)、锁要短(时长)、锁要少(频率)——违反任何一条,多线程就白写了
Arc<Mutex<T>>拆开用:别整个程序一把大锁(Arc<Mutex<Vec<...>>>全锁住),按数据分锁(每段数据一把小锁)- 锁里千万别调"可能等很久"的东西:网络请求、
recv()、大 IO——锁里只能做内存操作 - 看热点:用 profiling(第 8 节)看
lock的等待时间,比瞎猜准