21、并发调试与性能分析:GDB多线程调试、ThreadSanitizer、性能分析工具
说实话,并发编程写起来挺爽的,但调试起来……嗯,那真是另一回事了。
我记得刚入行那会儿,写了个多线程的缓存模块,跑了一整天都没事。结果上线第二天,凌晨三点,线上挂了。日志里啥也没留下,就是偶尔崩一下。那种感觉,就像你明明锁了门,但小偷还是进来了——你根本不知道漏洞在哪。
所以这一章,我把自己这些年踩过的坑、用顺手的工具,都给你捋一遍。说白了,就是三样东西:GDB多线程调试、ThreadSanitizer、性能分析工具。这三板斧用好了,并发问题基本无处藏身。
GDB多线程调试:别慌,先看堆栈
多线程程序崩溃了,第一反应是什么?我个人的习惯是:先别急着改代码,先看看所有线程在干嘛。
GDB里有个命令,我几乎每次都用——thread apply all bt。它的意思是:把所有线程的堆栈都打出来。你想想看,一个程序卡住了,到底是哪个线程在等锁?哪个线程死循环了?一眼就能看出来。
实战场景:程序卡死,疑似死锁。
(gdb) thread apply all bt
Thread 3 (Thread 0x7f...):
#0 __lll_lock_wait () at ...
#1 pthread_mutex_lock (mutex=0x...) at ...
#2 worker_thread (arg=0x...) at worker.c:42
Thread 2 (Thread 0x7f...):
#0 __lll_lock_wait () at ...
#1 pthread_mutex_lock (mutex=0x...) at ...
#2 worker_thread (arg=0x...) at worker.c:42
Thread 1 (Thread 0x7f...):
#0 pthread_join (th=...) at ...
看到没?两个线程都在等锁。这时候我一般会再检查一下锁的地址——如果两个线程等的是同一把锁,那基本就是死锁了。
我的小技巧:在GDB里设置条件断点,比如 break worker.c:42 if thread_id == 2。这样只有特定线程会停下来,其他线程继续跑。调试起来清爽很多。
还有一个我常用的:info threads。它会列出所有线程的ID和状态。配合 thread N 切换线程上下文,你可以挨个检查每个线程的局部变量。嗯,这招在排查“数据被谁改了”的时候特别好用。
ThreadSanitizer:自动抓数据竞争
说实话,GDB虽然强大,但有些问题它抓不到。比如数据竞争——两个线程同时读写同一个变量,没有加锁。这种问题在GDB里几乎看不出来,因为它是偶发的。
这时候就该 ThreadSanitizer(TSan) 上场了。它是LLVM/GCC内置的一个工具,编译时加个选项就行。
gcc -fsanitize=thread -g -o myapp myapp.c -lpthread
./myapp
跑起来之后,如果有数据竞争,它会直接打印警告。我记得有一次,我写了个共享计数器,忘了加锁。TSan直接报出来:
WARNING: ThreadSanitizer: data race (pid=12345)
Write of size 4 at 0x... by thread T2:
#0 increment_counter counter.c:15
Previous read of size 4 at 0x... by thread T1:
#0 get_counter counter.c:20
注意:TSan会拖慢程序运行速度,大概2-5倍。所以不要在线上环境开,只在测试阶段用。另外,TSan和AddressSanitizer(ASan)不能同时开,会冲突。
我个人习惯是:每次写完多线程代码,先用TSan跑一遍单元测试。哪怕没发现问题,心里也踏实。毕竟,数据竞争这种bug,线上出了就是事故。
性能分析工具:你的程序到底慢在哪?
并发程序写对了,不代表写好了。有时候你会发现,明明开了8个线程,CPU利用率就是上不去。或者某个线程卡得不行,其他线程都在等它。
这时候就需要性能分析工具了。我主要用两个:perf 和 gprof。
perf:轻量级采样分析
perf是Linux内核自带的工具,采样方式工作,对程序性能影响很小。我最常用的是:
perf record -g ./myapp
perf report
它会告诉你每个函数占了多少CPU时间。我曾经用perf发现一个“热点”——某个线程里有个自旋锁,忙等了大量时间。优化成条件变量后,性能直接翻倍。
锁竞争怎么看? perf top -p PID 可以实时看热点。如果看到 pthread_mutex_lock 出现在热点里,说明锁竞争严重。
gprof:函数调用关系图
gprof需要编译时加 -pg 选项,运行后会生成 gmon.out 文件。然后:
gprof ./myapp gmon.out > report.txt
它会生成一个调用关系图,告诉你每个函数被调用了多少次、花了多少时间。我个人觉得,gprof适合分析“函数调用链”问题,比如某个函数被频繁调用导致性能瓶颈。
避坑指南:我曾经在线上环境不小心跑了带 -pg 编译的程序,结果性能下降明显。所以记住——性能分析工具只在测试环境用。
总结一下
并发调试和性能分析,说白了就是三件事:
- GDB:看线程堆栈、查死锁、设条件断点。适合定位“卡死”和“崩溃”。
- ThreadSanitizer:自动检测数据竞争。适合发现“偶发”的并发bug。
- perf / gprof:找性能热点、分析锁竞争。适合优化“跑得慢”的问题。
这三样工具配合使用,大部分并发问题都能搞定。嗯,至少我这些年遇到的,基本没跑出这个范围。