C++ GDB 多线程调试:断点、线程切换与变量查看

各位观众,欢迎来到今天的GDB多线程调试脱口秀!今天的主题是“C++ GDB 多线程调试:断点、线程切换与变量查看”。别担心,虽然听起来像高级课程,但我保证让大家听得懂,学得会,甚至还能笑出声!

开场白:多线程的“甜蜜”烦恼

多线程编程就像同时耍好几个杂技球。刚开始觉得很酷炫,但一不小心,球就掉下来砸到脚了。在多线程程序中,bug往往藏得很深,就像躲猫猫高手,让你找得头昏眼花。这时候,GDB就是你的“金睛火眼”,能帮你揪出这些捣蛋鬼。

第一幕:断点,时间暂停的艺术

断点,顾名思义,就是让程序在某个地方停下来,让你有机会“冷静”地观察一下。在多线程环境中,断点就更有用了,它可以让你暂停所有线程,或者只暂停特定的线程。

  • 全局断点:一起停下来喝杯咖啡

    最简单的断点设置方式,就是让所有线程一起暂停。这就像在公司群里发通知:“全体员工,暂停工作,喝杯咖啡!”

    #include <iostream>
    #include <thread>
    #include <vector>
    
    void worker(int id) {
        for (int i = 0; i < 5; ++i) {
            std::cout << "Thread " << id << ": " << i << std::endl;
            // 这里设置断点
        }
    }
    
    int main() {
        std::vector<std::thread> threads;
        for (int i = 0; i < 3; ++i) {
            threads.emplace_back(worker, i);
        }
    
        for (auto& t : threads) {
            t.join();
        }
    
        return 0;
    }

    编译: g++ -g -pthread main.cpp -o main

    启动GDB: gdb ./main

    设置断点: break main.cpp:9 (在worker函数内部设置断点)

    运行: run

    当程序运行到断点处,所有线程都会暂停。你可以用 info threads 命令查看所有线程的状态。

  • 条件断点:只招待VIP线程

    有时候,你只想暂停特定的线程,比如某个线程出了问题,或者你想观察某个线程的特定行为。这时候,条件断点就派上用场了。这就像夜店,只招待VIP客人。

    // 假设线程id为0的线程有异常
    break main.cpp:9 thread id == 1

    上面的命令表示,只有当线程ID等于1时,程序才会暂停。id 是GDB内置的一个变量,表示当前线程的ID。

  • 数据断点:盯紧你的宝贝变量

    数据断点,也叫watchpoint,它会在某个变量的值发生变化时暂停程序。这就像给你的宝贝变量装了个监控摄像头,一旦发现异常,立即报警。

    #include <iostream>
    #include <thread>
    
    int global_var = 0;
    
    void worker() {
        for (int i = 0; i < 10; ++i) {
            global_var++;
            std::cout << "Global variable: " << global_var << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        }
    }
    
    int main() {
        std::thread t1(worker);
        std::thread t2(worker);
    
        t1.join();
        t2.join();
    
        return 0;
    }

    编译: g++ -g -pthread main.cpp -o main

    启动GDB: gdb ./main

    设置数据断点: watch global_var

    运行: run

    global_var的值发生变化时,程序就会暂停。

    注意:数据断点可能会比较慢,因为它需要不断地检查变量的值。

第二幕:线程切换,在不同角色间自由切换

在多线程调试中,你需要在不同的线程之间切换,以便观察它们的行为。这就像导演在不同的拍摄现场之间切换镜头,确保每个镜头都完美。

  • info threads:线程列表,点兵点将

    info threads 命令可以列出所有线程的信息,包括线程ID、状态和当前执行的代码行。这就像点名册,让你知道哪些线程在摸鱼,哪些线程在认真工作。

    (gdb) info threads
      Id   Target Id         Frame
        3    Thread 0x7ffff75f7700 (LWP 12345) 0x00007ffff7a00187 in nanosleep () from /lib64/libc.so.6
      * 1    Thread 0x7ffff7df8700 (LWP 12343) 0x00007ffff7a00187 in nanosleep () from /lib64/libc.so.6
        2    Thread 0x7ffff7df7700 (LWP 12344) 0x00007ffff7a00187 in nanosleep () from /lib64/libc.so.6

    星号(*)表示当前选中的线程。

  • thread <id>:切换线程,化身超级英雄

    thread <id> 命令可以让你切换到指定的线程。这就像变身,你可以瞬间变成任何一个线程,观察它的内部状态。

    (gdb) thread 2
    [Switching to thread 2 (Thread 0x7ffff7df7700 (LWP 12344))]
    #0  0x00007ffff7a00187 in nanosleep () from /lib64/libc.so.6

    上面的命令表示切换到线程ID为2的线程。

第三幕:变量查看,揭秘线程的内心世界

在多线程调试中,查看变量的值非常重要,它可以帮助你了解线程的内部状态,找出问题的原因。这就像心理医生,通过分析患者的言行举止,了解他们的内心世界。

  • print <variable>:简单粗暴,直接展示

    print <variable> 命令可以打印指定变量的值。这就像直接问问题,简单粗暴,但也很有效。

    #include <iostream>
    #include <thread>
    #include <mutex>
    
    int counter = 0;
    std::mutex mtx;
    
    void increment() {
        for (int i = 0; i < 100000; ++i) {
            std::lock_guard<std::mutex> lock(mtx);
            counter++;
        }
    }
    
    int main() {
        std::thread t1(increment);
        std::thread t2(increment);
    
        t1.join();
        t2.join();
    
        std::cout << "Counter: " << counter << std::endl; // 理想情况下应该是200000
    
        return 0;
    }

    编译: g++ -g -pthread main.cpp -o main

    启动GDB: gdb ./main

    设置断点: break main.cpp:13 (在 increment 函数的循环内部设置断点)

    运行: run

    当程序运行到断点处,你可以使用 print counter 命令查看 counter 的值。

  • display <variable>:持续关注,随时汇报

    display <variable> 命令可以让你持续关注某个变量的值,每次程序暂停时,都会自动显示该变量的值。这就像股票软件,实时更新股价,让你随时掌握市场动态。

    (gdb) display counter
    1: counter = 1234
  • ptype <variable>:知根知底,了解类型

    ptype <variable> 命令可以显示变量的类型。这就像查户口,让你了解变量的出身背景。

    (gdb) ptype counter
    type = int
  • 查看线程局部存储(TLS)变量

    线程局部存储(TLS)变量是每个线程独有的变量。在GDB中,你可以使用 thread <id> 切换到指定的线程,然后使用 print 命令查看该线程的TLS变量。

    #include <iostream>
    #include <thread>
    
    thread_local int tls_var = 0;
    
    void worker(int id) {
        tls_var = id * 10;
        std::cout << "Thread " << id << ": tls_var = " << tls_var << std::endl;
    }
    
    int main() {
        std::thread t1(worker, 1);
        std::thread t2(worker, 2);
    
        t1.join();
        t2.join();
    
        return 0;
    }

    编译: g++ -g -pthread main.cpp -o main

    启动GDB: gdb ./main

    设置断点: break main.cpp:9

    运行: run

    当程序运行到断点处,可以使用以下命令查看不同线程的 tls_var 的值:

    (gdb) thread 1
    (gdb) print tls_var
    $1 = 10
    (gdb) thread 2
    (gdb) print tls_var
    $2 = 20

高级技巧:GDB脚本,自动化调试

如果你需要进行复杂的调试,可以使用GDB脚本来自动化调试过程。GDB脚本可以包含一系列GDB命令,让你一次性执行多个操作。

  • 创建一个GDB脚本文件(例如:debug.gdb)

    break main.cpp:9
    commands
        info threads
        thread 1
        print counter
        continue
    end
    run

    上面的脚本表示:

    1. main.cpp:9 设置断点。
    2. 当程序运行到断点处,执行以下命令:
      • 显示所有线程的信息。
      • 切换到线程1。
      • 打印 counter 的值。
      • 继续运行程序。
    3. 运行程序。
  • 使用GDB脚本

    gdb ./main -x debug.gdb

    上面的命令表示使用 debug.gdb 脚本来调试 main 程序。

实战演练:死锁检测

死锁是多线程编程中常见的问题,它会导致程序卡死。GDB可以帮助你检测死锁。

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx1, mtx2;

void thread1() {
    mtx1.lock();
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟一些操作
    mtx2.lock();
    std::cout << "Thread 1: Acquired both locks" << std::endl;
    mtx2.unlock();
    mtx1.unlock();
}

void thread2() {
    mtx2.lock();
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟一些操作
    mtx1.lock();
    std::cout << "Thread 2: Acquired both locks" << std::endl;
    mtx1.unlock();
    mtx2.unlock();
}

int main() {
    std::thread t1(thread1);
    std::thread t2(thread2);

    t1.join();
    t2.join();

    return 0;
}

编译: g++ -g -pthread main.cpp -o main

运行: gdb ./main

运行程序直到死锁发生: run

如果程序卡死,按 Ctrl+C 中断程序。

使用 info threads 命令查看所有线程的状态。

(gdb) info threads
  Id   Target Id         Frame
    2    Thread 0x7ffff7df7700 (LWP 12344) 0x00007ffff7a00187 in nanosleep () from /lib64/libc.so.6
  * 1    Thread 0x7ffff7df8700 (LWP 12343) 0x00007ffff7a00187 in nanosleep () from /lib64/libc.so.6

可以使用thread apply all bt查看所有线程的堆栈信息:

(gdb) thread apply all bt

Thread 2 (Thread 0x7ffff7df7700 (LWP 12344)):
#0  0x00007ffff7a00187 in nanosleep () from /lib64/libc.so.6
#1  0x00007ffff7b0d5d8 in std::this_thread::sleep_for<std::chrono::duration<long, std::ratio<1l, 1000000000l> > > (__rtime=...) at /usr/include/c++/9/thread
#2  thread2 () at main.cpp:24
#3  0x00007ffff7b19609 in execute_native_thread_routine (__p=0x600000000260) at ../../../../src/libstdc++-v3/src/c++11/thread.cc:80
#4  0x00007ffff79f9ea7 in start_thread (arg=<optimized out>) at pthread_create.c:477
#5  0x00007ffff7ae8b0f in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

Thread 1 (Thread 0x7ffff7df8700 (LWP 12343)):
#0  0x00007ffff7a00187 in nanosleep () from /lib64/libc.so.6
#1  0x00007ffff7b0d5d8 in std::this_thread::sleep_for<std::chrono::duration<long, std::ratio<1l, 1000000000l> > > (__rtime=...) at /usr/include/c++/9/thread
#2  thread1 () at main.cpp:16
#3  0x00007ffff7b19609 in execute_native_thread_routine (__p=0x600000000240) at ../../../../src/libstdc++-v3/src/c++11/thread.cc:80
#4  0x00007ffff79f9ea7 in start_thread (arg=<optimized out>) at pthread_create.c:477
#5  0x00007ffff7ae8b0f in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

通过查看堆栈信息,你可以看到线程1正在等待 mtx2,而线程2正在等待 mtx1,从而确认死锁的发生。

总结:GDB,多线程调试的利器

GDB是多线程调试的利器,它可以让你暂停线程、切换线程、查看变量,从而了解线程的内部状态,找出问题的原因。掌握GDB的使用,可以大大提高你的多线程编程能力。

结束语:调试之路,永无止境

多线程调试是一个复杂的过程,需要不断学习和实践。希望今天的分享能帮助大家更好地掌握GDB的使用,成为多线程编程高手!记住,调试之路,永无止境,但只要你坚持下去,就能克服任何困难!

感谢各位的收看,我们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注