并发调试与性能分析:常见的并发bug与调试工具
并发编程,说白了就是让多个线程一起干活。但线程一多,问题就来了。我做了这么多年C++开发,可以负责任地告诉你:并发bug是最难复现、最难定位的那一类。今天我们就来聊聊这些坑,以及怎么用工具把它们揪出来。
核心要点:并发bug的三大噩梦——死锁、活锁、饥饿。以及两把利器——GDB调试、性能分析工具。
一、死锁:最经典的并发陷阱
死锁是什么?简单说就是两个线程互相等着对方释放资源,结果谁也动不了。我在项目中遇到过好几次,最惨的一次是线上服务挂了,查了半天才发现是死锁导致的。
死锁发生的四个必要条件,你想想看:
- 互斥条件:资源一次只能被一个线程占用
- 持有并等待:线程拿着一个资源,还在等另一个
- 不可剥夺:资源不能被强行拿走
- 循环等待:形成等待环路
来看一个典型的死锁代码:
// 线程1
std::mutex m1, m2;
void thread1() {
std::lock_guard<std::mutex> lock1(m1);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guard<std::mutex> lock2(m2); // 等m2
// 干活...
}
// 线程2
void thread2() {
std::lock_guard<std::mutex> lock2(m2);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guard<std::mutex> lock1(m1); // 等m1
// 干活...
}
为什么会死锁?线程1锁了m1等m2,线程2锁了m2等m1,两个人都卡住了。嗯,这里要注意:加锁顺序不一致是死锁的头号元凶。
我的习惯:项目中统一约定加锁顺序,比如总是先锁m1再锁m2。或者直接用std::lock一次性锁多个互斥量,避免顺序问题。
二、活锁与饥饿:没那么明显但同样致命
活锁比死锁更隐蔽。线程没卡死,但一直在做无用功。我记得有一次,两个线程互相谦让资源,结果谁也没干成活——就像两个人面对面让路,你往左我也往左,你往右我也往右。
饥饿就更常见了。某个线程优先级太低,或者调度策略不公平,导致它一直拿不到CPU时间。我曾经在项目中遇到过:一个后台处理线程因为优先级设置不当,在高负载下几乎得不到执行机会,任务越积越多。
避免饥饿的方法:
- 使用公平锁(如std::mutex本身就是公平的)
- 避免线程优先级差异过大
- 使用条件变量配合超时机制
三、GDB调试多线程程序
GDB是调试并发bug的利器。我个人习惯用这几个命令:
| 命令 | 作用 |
|---|---|
| info threads | 查看所有线程 |
| thread N | 切换到线程N |
| bt | 查看当前线程的调用栈 |
| thread apply all bt | 打印所有线程的调用栈 |
调试死锁时,我一般这样做:
- 程序卡住后,用
Ctrl+C中断 - 输入
thread apply all bt查看所有线程的调用栈 - 看每个线程在等哪个锁,哪个线程持有了这个锁
- 找到循环等待的证据
举个例子,如果看到线程1在__lll_lock_wait里,线程2也在__lll_lock_wait里,那基本就是死锁了。再结合bt看具体在等哪个互斥量,就能定位问题。
注意:调试release版本时,很多信息会被优化掉。建议在debug模式下编译,加-g -O0选项。
四、性能分析工具的使用
光能调试bug还不够,还得会分析性能。我常用的工具有这些:
- perf:Linux自带的性能分析工具,轻量级
- Valgrind:内存检测和性能分析,功能强大但慢
- gperftools:Google出品的CPU和堆分析工具
用perf分析并发程序的热点:
# 采样程序运行时的CPU事件
perf record -g ./my_program
# 生成报告
perf report
# 查看线程级别的调度情况
perf sched record ./my_program
perf sched latency
我记得有一次,一个多线程程序性能上不去。用perf一看,发现大量时间花在锁竞争上。说白了就是线程都在抢锁,真正干活的时间很少。后来改成无锁数据结构,性能直接翻倍。
五、知识体系总览
下面这张图总结了本章的核心内容,你可以对照着梳理思路:
六、避坑指南
最后分享几个我踩过的坑:
我曾经...在调试一个死锁时,用GDB看了半天调用栈,发现每个线程都在等锁,但就是看不出谁持有了锁。后来才发现,有个线程在加锁前就崩溃了,锁没释放。从那以后,我养成了用std::lock_guard的习惯,RAII机制能保证异常安全。
另一个教训:性能分析时别只看CPU使用率。有时候CPU跑满了,但实际都在做无用功(比如自旋锁忙等)。要用perf看具体在哪个函数上花时间,才能找到真正的瓶颈。
嗯,并发调试就是这样——理论要懂,工具要熟,经验要积累。多动手,多踩坑,慢慢就熟练了。