1. 调试基础:GDB入门与多线程调试环境搭建

1.1 为什么调试多线程程序这么难?

说实话,我刚开始接触多线程编程时,觉得这东西挺酷的。几个线程同时跑,效率多高啊!但真正上手后才发现——调试多线程程序,简直就是噩梦。

单线程程序出问题了,你打断点、单步执行、看变量值,基本能定位到问题。但多线程呢?线程A执行到一半,线程B突然把共享数据改了。你打断点?断点一触发,所有线程都停在那,上下文全乱了。你打印日志?日志本身就有IO延迟,打印出来的顺序可能根本不是实际执行顺序。

为什么会这样?说白了,多线程的「非确定性」是罪魁祸首。每次运行,线程调度顺序都可能不同。你今天复现的bug,明天可能就复现不了。我有个同事曾经为了一个死锁问题,连续加班三天,最后发现是线程加锁顺序不一致导致的。

所以,掌握多线程调试工具,是每个并发程序员的必修课。而GDB,就是我们的第一把利器。

核心观点:多线程调试的难点不在于工具本身,而在于「重现问题」。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 单步执行(进入函数)
print 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 知识体系总览

说了这么多,我画张图帮你理清思路:

多线程调试知识体系 GDB多线程调试 单线程调试基础 break / run / next step / print / continue 多线程核心命令 info threads thread / thread apply 高级调试技巧 线程特定断点 scheduler-locking 环境搭建 gcc -g -lpthread 生产者-消费者模型 常见问题排查 死锁 / 竞态条件 条件变量等待

1.6 避坑指南

最后,分享几个我踩过的坑:

  • 断点位置不对:我曾经在 pthread_create 之后立刻设断点,结果新线程还没创建,断点根本不会触发。正确做法是在线程函数入口设断点。
  • 忘记加 -g:编译时没加调试信息,GDB里看不到源码和变量名,只能看到汇编。嗯,那体验,简直酸爽。
  • 条件变量死锁:用 scheduler-locking on 时,如果当前线程在等锁,而被锁住的线程持有锁,程序就卡死了。这时候用 thread apply all bt 看看所有线程的调用栈,能快速定位。
我的建议:刚开始学多线程调试,别急着上复杂项目。就用生产者-消费者这种经典模型,反复练习 info threads、thread、scheduler-locking 这几个命令。练熟了,再上真实项目。

好了,这一章的内容就到这。GDB入门其实不难,难的是养成「调试思维」——遇到问题先想「我能不能用GDB复现它?」,而不是盲目改代码。下一章,我们会深入GDB的更多高级功能,比如条件断点、观察点、以及如何用GDB分析内存问题。

可可精品资料库