一、并发问题的三大类
所有并发 bug,本质都落在三类:
| 类型 | 症状 | 本质 |
|---|---|---|
| 数据竞争 | 结果偶现错误 | 未同步写 |
| 死锁 | 程序卡死 | 锁循环等待 |
| 性能退化 | CPU 高 / 卡顿 | 锁竞争严重 |
你必须学会区分它们。
二、数据竞争(Race Condition)
典型症状:
- 本地跑正常
- Release 才出问题
- 多核机器更容易复现
- 偶现 bug
示例
cpp
if (counter > 0) {
counter--;
}
即使 counter 是 atomic,也不安全。
因为:
判断 + 修改 不是原子事务。
如何定位?
1️⃣ 检查是否有共享变量
2️⃣ 是否存在写操作
3️⃣ 是否所有访问都在锁内
工程纪律:
所有共享变量必须有"唯一保护策略"。
不要:
- 一部分用锁
- 一部分不用锁
- 一部分用 atomic
必须统一。
三、死锁(Deadlock)
死锁的四个必要条件:
1️⃣ 互斥
2️⃣ 持有并等待
3️⃣ 不可抢占
4️⃣ 循环等待
只要破坏一个条件,就不会死锁。
经典死锁例子
cpp
// 线程 A
lock(m1);
lock(m2);
// 线程 B
lock(m2);
lock(m1);
解决方案 1:统一锁顺序
永远按顺序:
cpp
lock(m1);
lock(m2);
解决方案 2:std::lock
cpp
std::unique_lock<std::mutex> lock1(m1, std::defer_lock);
std::unique_lock<std::mutex> lock2(m2, std::defer_lock);
std::lock(lock1, lock2);
避免循环等待。
四、锁竞争(性能问题)
症状:
- CPU 高
- 线程多但吞吐不涨
- perf 显示大量等待
锁太大
cpp
std::lock_guard lock(mtx);
doHeavyTask();
错误。
正确方式
cpp
{
std::lock_guard lock(mtx);
popTask();
}
doHeavyTask();
工程优化策略
1️⃣ 缩小临界区
2️⃣ 分段锁(多个 mutex)
3️⃣ 读写锁(shared_mutex)
4️⃣ 避免过度 atomic 自旋
五、条件变量常见坑
❌ 用 if 而不是 while
必须:
cpp
cv.wait(lock, condition);
❌ 在锁外修改状态
状态必须在锁内更新。
❌ 忘记 notify
所有改变条件的操作必须通知。
六、atomic 的常见误用
误区 1:用 atomic 保护容器
std::atomic<int> size;
size 正确,不代表 vector 安全。
误区 2:用 atomic 替代锁
atomic 不能做事务。
七、线程生命周期问题
1️⃣ 忘记 join
程序崩溃。
2️⃣ detach 滥用
线程失控。
原则:
所有线程必须有明确 owner。
八、并发调试方法论
1️⃣ 先确认问题类型
卡死?
数据错?
性能慢?
2️⃣ 打印线程 id
cpp
std::this_thread::get_id();
3️⃣ 加日志定位锁持有时间
4️⃣ 使用工具(进阶)
- ThreadSanitizer
- valgrind
- perf
- gdb bt
九、工程级并发纪律清单(必须收藏)
资源层
✅ 每个共享资源有唯一 mutex
✅ 不允许"有时加锁,有时不加锁"
等待层
✅ wait 必须带 predicate
✅ 状态必须锁内修改
✅ 修改后必须 notify
状态层
✅ atomic 只用于简单状态
✅ 不做复杂逻辑
生命周期
✅ 所有线程必须 join
✅ shutdown 必须可控
十、Java 对比总结
Java:
- 有 GC
- 有成熟线程池
- 有 JVM 优化
C++:
- 更底层
- 更危险
- 更可控
你写的每一行锁:
都是 JVM 在 Java 里帮你干的事情。
十一、并发系统最终模型(终章脑图)
你现在脑子里应该是这样:
cpp
线程模型
↓
共享资源
↓
mutex 保护
↓
condition_variable 协作
↓
atomic 状态控制
↓
生命周期管理
↓
排障与优化
这就是:
并发系统取向思维。
十二、全系列总结一句话
线程私有栈,共享堆
共享必保护
保护用 mutex
协作用 cv
状态用 atomic
生命周期可控
锁只保护资源,不保护业务
十三、你现在是什么水平?
如果你完全理解这 7 篇:
你已经:
✔ 不再是"会写 thread 的人"
✔ 而是具备"并发系统思维的人"
这已经是:
C++ 工程中级偏上水平的并发基础。