1. 调试基础:GDB入门与多线程调试环境搭建
1.1 为什么调试多线程程序这么难?
说实话,我刚开始接触多线程编程时,觉得这东西挺酷的。几个线程同时跑,效率多高啊!但真正上手后才发现——调试多线程程序,简直就是噩梦。
单线程程序出问题了,你打断点、单步执行、看变量值,基本能定位到问题。但多线程呢?线程A执行到一半,线程B突然把共享数据改了。你打断点?断点一触发,所有线程都停在那,上下文全乱了。你打印日志?日志本身就有IO延迟,打印出来的顺序可能根本不是实际执行顺序。
为什么会这样?说白了,多线程的「非确定性」是罪魁祸首。每次运行,线程调度顺序都可能不同。你今天复现的bug,明天可能就复现不了。我有个同事曾经为了一个死锁问题,连续加班三天,最后发现是线程加锁顺序不一致导致的。
所以,掌握多线程调试工具,是每个并发程序员的必修课。而GDB,就是我们的第一把利器。
1.2 GDB入门:从单线程开始
先别急着搞多线程。我建议你先把单线程的GDB玩熟。就像学开车,先练好直线行驶,再学倒车入库。
1.2.1 安装与基本命令
Linux系统下,GDB基本都预装了。没装的话,一行命令搞定:
sudo apt-get install gdb # Ubuntu/Debian
sudo yum install gdb # CentOS/RHEL
写个简单的测试程序:
// test.c
#include <stdio.h>
int add(int a, int b) {
int result = a + b;
return result;
}
int main() {
int x = 10;
int y = 20;
int z = add(x, y);
printf("z = %d\n", z);
return 0;
}
编译时记得加 -g 选项,保留调试信息:
gcc -g -o test test.c
启动GDB:
gdb ./test
常用的几个命令,我列个表给你:
| 命令 | 缩写 | 作用 |
|---|---|---|
| break | b | 设置断点 |
| run | r | 运行程序 |
| next | n | 单步执行(跳过函数) |
| step | s | 单步执行(进入函数) |
| p | 打印变量值 | |
| continue | c | 继续执行 |
| quit | q | 退出GDB |
实际操作一下:
(gdb) b main # 在main函数入口设断点
(gdb) r # 运行,停在main入口
(gdb) n # 单步,执行int x = 10;
(gdb) p x # 打印x的值,输出 $1 = 10
(gdb) n # 执行int y = 20;
(gdb) p y # 打印y的值,输出 $2 = 20
(gdb) s # 进入add函数
(gdb) p a # 打印参数a的值
(gdb) p b # 打印参数b的值
(gdb) n # 执行int result = a + b;
(gdb) p result # 打印result的值
(gdb) c # 继续执行到结束
layout split 命令,可以同时看到源码和GDB命令行,调试体验好很多。你试试看。
1.3 多线程调试环境搭建
单线程玩熟了,咱们上多线程。先搭个环境,写个经典的生产者-消费者模型:
// multi_thread.c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int count = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_producer = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond_consumer = PTHREAD_COND_INITIALIZER;
void* producer(void* arg) {
int item = 0;
while (1) {
pthread_mutex_lock(&mutex);
while (count == BUFFER_SIZE) {
pthread_cond_wait(&cond_producer, &mutex);
}
buffer[count++] = item++;
printf("Produced: %d, count = %d\n", item - 1, count);
pthread_cond_signal(&cond_consumer);
pthread_mutex_unlock(&mutex);
usleep(100000); // 模拟生产耗时
}
return NULL;
}
void* consumer(void* arg) {
while (1) {
pthread_mutex_lock(&mutex);
while (count == 0) {
pthread_cond_wait(&cond_consumer, &mutex);
}
int item = buffer[--count];
printf("Consumed: %d, count = %d\n", item, count);
pthread_cond_signal(&cond_producer);
pthread_mutex_unlock(&mutex);
usleep(150000); // 模拟消费耗时
}
return NULL;
}
int main() {
pthread_t prod, cons;
pthread_create(&prod, NULL, producer, NULL);
pthread_create(&cons, NULL, consumer, NULL);
pthread_join(prod, NULL);
pthread_join(cons, NULL);
return 0;
}
编译时记得链接pthread库:
gcc -g -o multi_thread multi_thread.c -lpthread
1.4 GDB多线程调试核心命令
好,程序跑起来了。现在用GDB调试它。启动方式一样:
gdb ./multi_thread
多线程调试有几个关键命令,我一个个说:
1.4.1 查看线程信息
(gdb) info threads
Id Target Id Frame
* 1 Thread 0x7ffff7fc1740 (LWP 12345) "multi_thread" main () at multi_thread.c:40
2 Thread 0x7ffff6dff700 (LWP 12346) "multi_thread" producer () at multi_thread.c:12
3 Thread 0x7ffff65fe700 (LWP 12347) "multi_thread" consumer () at multi_thread.c:26
看到没?三个线程:主线程、生产者、消费者。前面的 * 表示当前焦点线程。
1.4.2 切换线程
(gdb) thread 2 # 切换到生产者线程
(gdb) thread 3 # 切换到消费者线程
(gdb) thread apply all bt # 打印所有线程的调用栈
嗯,这里有个坑。我曾经调试一个死锁问题,用 thread apply all bt 一看,生产者线程卡在 pthread_cond_wait,消费者线程也卡在 pthread_cond_wait。两个线程都在等条件变量,但没人发信号。后来发现是条件变量的逻辑写反了。
1.4.3 设置线程特定断点
(gdb) break producer if count == 3 # 只在count等于3时停住
(gdb) break consumer thread 3 # 只在消费者线程停住
这个功能太实用了。你想啊,如果所有线程都在同一个断点停住,你根本分不清是哪个线程触发的。指定线程断点,精准定位。
1.4.4 锁定线程调度
这是GDB多线程调试的「杀手锏」。有时候你想单步调试某个线程,但其他线程还在跑,会干扰你的观察。这时候可以用:
(gdb) set scheduler-locking on # 锁定调度,只运行当前线程
(gdb) set scheduler-locking step # 单步时锁定,continue时放开
我个人的习惯是:调试初期用 scheduler-locking on,先搞清楚一个线程的执行路径。等逻辑理顺了,再关掉锁定,观察多线程交互。
1.5 知识体系总览
说了这么多,我画张图帮你理清思路:
1.6 避坑指南
最后,分享几个我踩过的坑:
- 断点位置不对:我曾经在
pthread_create之后立刻设断点,结果新线程还没创建,断点根本不会触发。正确做法是在线程函数入口设断点。 - 忘记加
-g:编译时没加调试信息,GDB里看不到源码和变量名,只能看到汇编。嗯,那体验,简直酸爽。 - 条件变量死锁:用
scheduler-locking on时,如果当前线程在等锁,而被锁住的线程持有锁,程序就卡死了。这时候用thread apply all bt看看所有线程的调用栈,能快速定位。
info threads、thread、scheduler-locking 这几个命令。练熟了,再上真实项目。
好了,这一章的内容就到这。GDB入门其实不难,难的是养成「调试思维」——遇到问题先想「我能不能用GDB复现它?」,而不是盲目改代码。下一章,我们会深入GDB的更多高级功能,比如条件断点、观察点、以及如何用GDB分析内存问题。