21、并发调试与性能分析:GDB多线程调试、ThreadSanitizer、性能分析工具

说实话,并发编程写起来挺爽的,但调试起来……嗯,那真是另一回事了。

我记得刚入行那会儿,写了个多线程的缓存模块,跑了一整天都没事。结果上线第二天,凌晨三点,线上挂了。日志里啥也没留下,就是偶尔崩一下。那种感觉,就像你明明锁了门,但小偷还是进来了——你根本不知道漏洞在哪。

所以这一章,我把自己这些年踩过的坑、用顺手的工具,都给你捋一遍。说白了,就是三样东西:GDB多线程调试ThreadSanitizer性能分析工具。这三板斧用好了,并发问题基本无处藏身。

并发调试与性能分析知识体系 核心 GDB多线程调试 thread apply all bt 断点 + 条件断点 锁状态检查 ThreadSanitizer -fsanitize=thread 数据竞争检测 死锁检测 性能分析工具 perf / gprof 热点函数定位 锁竞争分析 三板斧:定位 → 检测 → 优化

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利用率就是上不去。或者某个线程卡得不行,其他线程都在等它。

这时候就需要性能分析工具了。我主要用两个:perfgprof

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:找性能热点、分析锁竞争。适合优化“跑得慢”的问题。

这三样工具配合使用,大部分并发问题都能搞定。嗯,至少我这些年遇到的,基本没跑出这个范围。

公众号:蓝海资料掘金营,微信deep3321