并发 Bug 排查
问题
Rust 项目中如何排查和预防并发 Bug?
答案
Rust 能防止的 vs 不能防止的
| 类别 | Rust 编译器能防止 | Rust 无法防止 |
|---|---|---|
| 数据竞争 | ✅ 借用规则 + Send/Sync | |
| 逻辑竞态 | ❌ TOCTOU、非原子复合操作 | |
| 死锁 | ❌ 锁顺序问题 | |
| 活锁 | ❌ 互相让步 |
逻辑竞态条件
use std::sync::Mutex;
// ❌ TOCTOU(Time-of-Check-to-Time-of-Use)
fn bad_increment(counter: &Mutex<HashMap<String, i32>>, key: &str) {
let map = counter.lock().unwrap();
let count = map.get(key).copied().unwrap_or(0);
drop(map); // 释放锁
// 其他线程可能在此时修改了 map!
let mut map = counter.lock().unwrap();
map.insert(key.to_string(), count + 1); // 可能覆盖其他线程的写入
}
// ✅ 保持锁的持有期间完成读写
fn good_increment(counter: &Mutex<HashMap<String, i32>>, key: &str) {
let mut map = counter.lock().unwrap();
let count = map.entry(key.to_string()).or_insert(0);
*count += 1;
// 锁在作用域结束时释放
}
原子操作顺序问题
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
struct Metrics {
ready: AtomicBool,
value: AtomicU64,
}
// ❌ Relaxed 无法保证跨变量的顺序
fn bad_publish(m: &Metrics) {
m.value.store(42, Ordering::Relaxed);
m.ready.store(true, Ordering::Relaxed);
}
fn bad_read(m: &Metrics) -> Option<u64> {
if m.ready.load(Ordering::Relaxed) {
Some(m.value.load(Ordering::Relaxed)) // 可能读到旧值!
} else { None }
}
// ✅ 使用 Release/Acquire 保证顺序
fn good_publish(m: &Metrics) {
m.value.store(42, Ordering::Relaxed);
m.ready.store(true, Ordering::Release); // Release 保证之前的写入可见
}
fn good_read(m: &Metrics) -> Option<u64> {
if m.ready.load(Ordering::Acquire) { // Acquire 与 Release 配对
Some(m.value.load(Ordering::Relaxed))
} else { None }
}
异步并发陷阱
use tokio::sync::Mutex as AsyncMutex;
use std::sync::Mutex as StdMutex;
// ❌ 标准 Mutex 跨 await —— 可能导致死锁
async fn bad(data: &StdMutex<Vec<i32>>) {
let mut guard = data.lock().unwrap();
some_async_fn().await; // 持有标准锁跨 await!
guard.push(1);
}
// ✅ 使用 tokio::sync::Mutex
async fn good(data: &AsyncMutex<Vec<i32>>) {
let mut guard = data.lock().await;
some_async_fn().await;
guard.push(1);
}
// ✅ 或者缩小锁作用域
async fn also_good(data: &StdMutex<Vec<i32>>) {
{
let mut guard = data.lock().unwrap();
guard.push(1);
} // 锁在 await 之前释放
some_async_fn().await;
}
排查工具
| 工具 | 用途 |
|---|---|
--cfg loom / loom crate | 并发正确性模型检查 |
ThreadSanitizer (TSan) | 运行时数据竞争检测 |
tokio-console | 异步任务监控 |
tracing | 结构化日志追踪并发流程 |
常见面试问题
Q1: Rust 消除了数据竞争,还需要担心并发 Bug 吗?
答案:
需要。Rust 消除的是 data race(对同一内存的无同步并发读写),但无法防止 race condition(逻辑层面的竞态)。例如:
- TOCTOU 问题(check-then-act 之间状态改变)
- 死锁(多锁乱序)
- 逻辑不一致(多个原子操作之间无整体原子性)
Q2: 什么情况下会出现 Rust 的并发 Bug?
答案:
- unsafe 代码绕过借用检查
- Mutex 锁粒度不当导致 TOCTOU
- 原子操作 Ordering 选择错误导致可见性问题
- 死锁(多锁交叉持有)
- Channel 使用不当(未消费导致内存增长、意外 drop sender)