• C++(Qt)软件调试---线程死锁调试(15)


    C++(Qt)软件调试—线程死锁调试(15)

    1、前言

    死锁是一种情况,其中两个或多个线程(或进程)相互等待对方释放资源,导致它们都无法继续执行。这是一种非常令人头疼的问题,因为它可以导致程序挂起,无法继续运行。

    本文中会详细讲述linux、Windows下调试C++线程死锁、Qt线程死锁的方式。

    • 系统环境:ubuntu20.04、Windows10;
    • 编译器:g++10、MinGW、MSVC2017-64;
    • 调试工具:gdb、WinDbg。
    • 所有程序编译时最好加上调试信息,如果是使用Qt,则使用Debug或者Profile模式。
    • 文中用到的方法也适用于调试死循环,不过细节上有一点点区别。

    2、常见死锁

    单线程死锁
    有时候,线程申请了锁资源,还没有等待释放,又一次申请这把锁,结果就是挂起等待这把锁的释放,但是这把锁是被自己拿着,所以就会永远挂起等待,就造成了死锁。导致重复加锁的原因可能如下:

    • 通常会因为在多分支中加锁,而某个分支忘记了加锁或者因为return、break等语句跳过了锁的释放;
    • 因为程序中自己使用throw抛出异常或者底层库抛出异常,打乱了程序的执行流程,导致锁没有释放。

    例如,考虑以下伪代码:

    void threadFun1()
    {
        g_mutex1.lock();        // 加锁
        
        g_mutex1.lock();        // 重复加锁
    
        g_mutex1.unlock();
    }
    
    void threadFun1()
    {
        g_mutex1.lock();        // 加锁
        
        if(value > 10) 
        {
            return;           // 提前返回,跳过释放
        }
    
        g_mutex1.unlock();
    }
    
    void threadFun1()
    {
        g_mutex1.lock();        // 加锁
        
        if(value > 10) 
        {
            throw;             // 抛出异常,打乱执行流程,跳过释放
        }
    
        g_mutex1.unlock();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    多线程死锁
    多线程死锁是更常见的情况,通常在多个线程之间共享资源时发生,也比单线程死锁更难排查。

    多线程死锁是指两个或多个线程在等待对方释放资源时被阻塞,无法继续执行。

    例如:线程1锁定了lock1并尝试获取lock2,而线程2锁定了lock2并尝试获取lock1,它们彼此等待对方释放资源,从而导致死锁。

    /********************************************************************************
    * 文件名:   main1.cpp
    * 创建时间: 2023-10-25 10:57:54
    * 开发者:   MHF
    * 邮箱:     1603291350@qq.com
    * 功能:     多线程死锁示例
    *********************************************************************************/
    #include 
    #include 
    #include 
    #include 
    
    using namespace std;
    mutex mutex1;
    mutex mutex2;
    
    void threadA()
    {
        cout << "启动线程A" << endl;
    
        mutex1.lock();
        cout << "线程A上锁mutex1" << endl;
    
        // 为了模拟死锁,让线程A休眠一段时间
        sleep(1);
    
        mutex2.lock();                        // 由于线程B已经上锁mutex2,这里会等待线程B解锁
        cout << "线程A上锁mutex2" << endl;
    
        // 执行一些操作...
    
        mutex2.unlock();
        mutex1.unlock();
    }
    
    void threadB()
    {
        cout << "启动线程B" << endl;
    
        mutex2.lock();
        cout << "线程B上锁 mutex2" << endl;
    
        // 为了模拟死锁,让线程B休眠一段时间
        sleep(1);
    
        mutex1.lock();                      // 由于线程A已经上锁mutex1,这里会等待线程A解锁
        cout << "线程B上锁 mutex1" << endl;
    
        // 执行一些操作...
    
        mutex1.unlock();
        mutex2.unlock();
    }
    
    int main()
    {
        thread t1(threadA);
        thread t2(threadB);
    
        t1.join();
        t2.join();
    
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    3、linux下gdb调试C++死锁

    1.1 使用代码

    	/********************************************************************************
    * 文件名:   main.cpp
    * 创建时间: 2023-10-24 21:40:05
    * 开发者:   MHF
    * 邮箱:     1603291350@qq.com
    * 功能:     单线程死锁示例
    *********************************************************************************/
    #include
    #include 
    #include 
    
    using namespace std;
    
    mutex g_mutex1;
    
    void threadFun1()
    {
        cout << 1 << endl;
        g_mutex1.lock();        // 加锁
        cout << 2 << endl;
        g_mutex1.lock();        // 重复加锁
        cout << 3 << endl;
    }
    
    
    int main()
    {
        thread t1(threadFun1);
    
        t1.join();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    1.2 gdb调试

    1. 使用g++ -g main.cpp -lpthread命令编译代码;

    2. 使用./a.out运行程序,会发现程序出现死锁,不会继续执行;

      在这里插入图片描述

    3. 重新打开一个终端窗口;

    4. 使用ps -aux | grep "a.out\|USER"命令查看a.out程序的进程信息(注意:\| 前后不能有空格);

      • grep “a.out \| USER”:表示只显示包含a.out字符串或者USER字符串的行;

      在这里插入图片描述

    5. 使用sudo gdb -q -p 14742将gdb附加到a.out的进程PID上(注意附加到进程需要使用sudo);

    6. 进入gdb后使用info threads命令查看所有线程的信息;

      在这里插入图片描述

    7. 从图中可以看出在线程2的堆栈停止在了**__lll_lock_wait**帧,在这个位置使用了g_mutex1锁,__lll_lock_wait函数是Linux系统中用于实现线程互斥锁等待的函数,它使线程进入等待状态,直到互斥锁可用。

    8. 使用thread 2命令进入到线程2中;

    9. 使用bt命令查看线程2当前的堆栈信息(也可以使用thread apply all bt命令查看所有线程的堆栈);

      在这里插入图片描述

    10. 可以堆栈停止在main.cpp文件的第21行,threadFun1()函数中;

    11. 使用f 4命令切换到线程2堆栈的第4帧,可以看见是停止在g_mutex1.lock()这一行加锁的代码上;

    12. 使用list命令查看上下文代码,可以看见加锁了两次;

    13. 使用p g_mutex1命令打印锁的信息可以看见__lock = 2也是加锁了两次。

      在这里插入图片描述

    3、linux下gdb调试Qt死锁

    1.1 使用代码

    #include "widget.h"
    #include "ui_widget.h"
    #include 
    #include 
    
    QMutex g_mutex;
    
    Widget::Widget(QWidget *parent)
        : QWidget(parent)
        , ui(new Ui::Widget)
    {
        ui->setupUi(this);
    }
    
    Widget::~Widget()
    {
        delete ui;
    }
    
    
    void Widget::on_pushButton_clicked()
    {
        // 创建一个QtConcurrent线程
        QtConcurrent::run(QThreadPool::globalInstance(), [&]()
        {
            qDebug() << "进入QtConcurrent线程";
            g_mutex.lock();
            qDebug() << "加锁1次";
            g_mutex.lock();
            qDebug() << "加锁2次,重复加锁";
    
            g_mutex.unlock();
        });
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    1.2 gdb调试

    1. 编译运行Qt程序后,点击pushButton按键,进入QtConcurrent线程,触发死锁;

    2. 使用ps -aux | grep 'testMutex\|USER'命令查看死锁进程pid;

    3. 使用sudo gdb -q -p 21714命令将gdb附加到进程;

    4. 使用info threads命令查看所有线程的信息;

    5. 如下图所示,可看出线程7的类型为Thread(pooled)(如果是使用QThread创建的线程这里类型就是QThread),这是使用线程池创建的QtConcurren线程,停止的堆栈帧的状态为syscall();程序停在syscall()函数通常意味着它正在进行系统调用,而如果出现死锁后线程就会一直处于这种状态;

      在这里插入图片描述

    6. 使用thread 7命令切换到线程7;

    7. 使用bt命令查看线程7堆栈信息;

    8. 如下图所示,利用看出QBasicMutex::lockInternal()或者QMutex::lock(),表示线程7堆栈停止在互斥锁的lock()函数位置,如何找到包含自己源代码的堆栈帧,在widget.cpp文件的29行。

      在这里插入图片描述

    9. 使用f 3命令切换到堆栈的第3帧,可以看的这一帧停止在g_mutex.lock()位置,正在加锁位置;

    10. 使用list命令查看上下文代码,可以看出加锁两次;

    11. 使用p g_mutex命令打印g_mutex锁的信息,和c++中的mutex锁不同,QMutex锁打印无法获得有帮助的信息。

      在这里插入图片描述

    4、Windows下gdb调试C++死锁

    使用代码和linux下一样。

    1. 打开MinGW-64的cmd窗口(从这里打开具有完整的环境变量,便于找到依赖库);

    在这里插入图片描述

    1. 进入到源代码所在路径;

    2. 使用g++.exe main.cpp -g -lpthread命令编译代码(如果提升找不到g++则使用MinGw所在绝对路径);

    3. 执行a.exe程序,触发死锁;

    在这里插入图片描述

    1. 打开任务管理器,找到a.exe程序,右键选择【转到详细信息】,查看进程的pid号,

    在这里插入图片描述

    1. 再打开一个cmd窗口;

    2. 使用gdb -q -p 8740将gdb附加到进程调试;

    3. 使用info threads命令查看所有线程信息(和linux下不同,不能直接看出死锁线程);

    在这里插入图片描述

    1. 使用thread apply all bt查看所有线程的堆栈信息;

    2. 如下图所示可以看出在线程2中出现了pthread_mutex_lock(),表示这个线程的堆栈停止在上锁位置,所以出现死锁,再往下找发现死锁位置出现在main.cpp文件的第21行中,threadFun1()函数位置。

      在这里插入图片描述

    3. 后面操作就可有可无了,并且和linux下没有什么区别;

    在这里插入图片描述

    5、Windows下gdb调试Qt死锁

    使用代码和linux下的相同;

    注意:Windows下使用MinGW编译程序,调试时选择的gdb版本应该和编译的g++版本相同,不能使用32位的gdb调试64位的程序,或者相反。

    1. Qt编译运行程序后,触发死锁;

    2. 打开对应版本的MinGW的cmd终端;

    3. 使用任务管理器窗口死锁程序的pid进程号;

    4. 使用gdb -q -p pid将gdb附加到死锁进程;

    5. 直接使用thread apply all bt显示所有线程的堆栈信息;

      在这里插入图片描述

    6. 可以看出线程3出现死锁,后续操作都是一样的。

    7. 不过MinGW中gdb调试有时会出现下列情况,无法进行调试,目前没找到问题;

      在这里插入图片描述

    6、Windows下Windbg调试C++死锁

    1.1 使用代码

    • 直接使用C++中的mutex锁重复上锁在msvc编译器中会在触发时抛出异常,所以无需调试。
    • 这里改为使用多线程死锁进行演示。
    /********************************************************************************
    * 文件名:   main.cpp
    * 创建时间: 2023-10-25 10:57:54
    * 开发者:   MHF
    * 邮箱:     1603291350@qq.com
    * 功能:     多线程死锁示例
    *********************************************************************************/
    #include 
    #include 
    #include 
    #include 
    
    using namespace std;
    mutex mutex1;
    mutex mutex2;
    
    void threadA()
    {
        cout << "start A" << endl;
    
        mutex1.lock();
        cout << "threadA mutex1 lock" << endl;
    
        // 为了模拟死锁,让线程A休眠一段时间
        Sleep(1000);
    
        mutex2.lock();                        // 由于线程B已经上锁mutex2,这里会等待线程B解锁
        cout << "threadA mutex2 lock" << endl;
    
        // 执行一些操作...
    
        mutex2.unlock();
        mutex1.unlock();
    }
    
    void threadB()
    {
        cout << "start B" << endl;
    
        mutex2.lock();
        cout << "threadB mutex2 lock" << endl;
    
        // 为了模拟死锁,让线程B休眠一段时间
        Sleep(1000);
    
        mutex1.lock();                      // 由于线程A已经上锁mutex1,这里会等待线程A解锁
        cout << "threadB mutex1 lock" << endl;
    
        // 执行一些操作...
    
        mutex1.unlock();
        mutex2.unlock();
    }
    
    int main()
    {
        thread t1(threadA);
        thread t2(threadB);
    
        t1.join();
        t2.join();
    
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    1.2 Windbg调试

    1. 使用MSVC编译器编译代码,运行并触发死锁;

    2. 打开WinDbg程序,(在C:\Program Files\Windows Kits\10\Debuggers\x64路径下);

    3. 选择【File】->【Attach to Process】或者直接按快捷键F6;

      在这里插入图片描述

    4. 然后选择By ID,找到死锁进程,然后点击【OK】;

      在这里插入图片描述

    5. 然后输入~*k命令查看所有线程的堆栈信息,如下所示出现std::_Mutex_base::lock字样,可看出在线程1、2出现死锁;

    在这里插入图片描述

    1. 然后选择【View】,打开【Processes and Threads】窗口和【Calls Stack】窗口;

    2. 点击【Processes and Threads】窗口中的线程1,再点击【Calls Stack】窗口中的堆栈帧,就可以跳转到出现死锁的源码位置;

    在这里插入图片描述

    1. 或者直接点击Command窗口中的堆栈帧也可以跳转到死锁源码位置(不过在WinDbg中定位到源码的位置是实际位置的下一行)。

    在这里插入图片描述

    7、Windows下Windbg调试Qt死锁

    使用代码和Linux下的相同;

    1. 前面步骤都是相同的;

    2. 在使用~*k命令窗口所有线程的堆栈信息时会发现看不到太多有帮助的信息,这时可用找包含源码文件的堆栈帧;

    在这里插入图片描述

    1. 如图所示,点击这一帧就可以跳转到源码查看是否时出现死锁的位置;

    在这里插入图片描述

    1. 如果想要查看更加详细的调试信息,需要到Qt官网下载Qt库的调试符号。

    {__/}
    (̷ ̷´̷ ̷^̷ ̷`̷)̷◞~❤
    | ⫘ |

  • 相关阅读:
    能源管理零代码开发工具助力“工业节能诊断服务”推进“双碳”目标顺利实现
    阿里云Redis性能压力测试(二十)
    Android:安卓虚拟机(Dalvik && ART)学习
    【编译原理】Antlr 入门使用
    让你全方位了解Shell终端,轻松学习
    linux简介
    铕掺杂稀土氧化物空心荧光微球/含稀土配合物荧光聚苯乙烯微球/荧光双响应稀土钒酸盐微球的制备方法
    简易版 图书管理系统
    RabbitMQ 26问,基本涵盖了面试官必问的面试题
    Java版工程行业管理系统源码-专业的工程管理软件- 工程项目各模块及其功能点清单
  • 原文地址:https://blog.csdn.net/qq_43627907/article/details/134036807