死锁是一种情况,其中两个或多个线程(或进程)相互等待对方释放资源,导致它们都无法继续执行。这是一种非常令人头疼的问题,因为它可以导致程序挂起,无法继续运行。
本文中会详细讲述linux、Windows下调试C++线程死锁、Qt线程死锁的方式。
单线程死锁:
有时候,线程申请了锁资源,还没有等待释放,又一次申请这把锁,结果就是挂起等待这把锁的释放,但是这把锁是被自己拿着,所以就会永远挂起等待,就造成了死锁。导致重复加锁的原因可能如下:
例如,考虑以下伪代码:
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锁定了lock1
并尝试获取lock2
,而线程2锁定了lock2
并尝试获取lock1
,它们彼此等待对方释放资源,从而导致死锁。
/********************************************************************************
* 文件名: main1.cpp
* 创建时间: 2023-10-25 10:57:54
* 开发者: MHF
* 邮箱: [email protected]
* 功能: 多线程死锁示例
*********************************************************************************/
#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;
}
/********************************************************************************
* 文件名: main.cpp
* 创建时间: 2023-10-24 21:40:05
* 开发者: MHF
* 邮箱: [email protected]
* 功能: 单线程死锁示例
*********************************************************************************/
#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;
}
使用g++ -g main.cpp -lpthread
命令编译代码;
使用./a.out
运行程序,会发现程序出现死锁,不会继续执行;
重新打开一个终端窗口;
使用ps -aux | grep "a.out\|USER"
命令查看a.out程序的进程信息(注意:\|
前后不能有空格);
使用sudo gdb -q -p 14742
将gdb附加到a.out的进程PID上(注意附加到进程需要使用sudo);
进入gdb后使用info threads
命令查看所有线程的信息;
从图中可以看出在线程2的堆栈停止在了**__lll_lock_wait**帧,在这个位置使用了g_mutex1锁,__lll_lock_wait函数是Linux系统中用于实现线程互斥锁等待的函数,它使线程进入等待状态,直到互斥锁可用。
使用thread 2
命令进入到线程2中;
使用bt
命令查看线程2当前的堆栈信息(也可以使用thread apply all bt
命令查看所有线程的堆栈);
可以堆栈停止在main.cpp文件的第21行,threadFun1()函数中;
使用f 4
命令切换到线程2堆栈的第4帧,可以看见是停止在g_mutex1.lock()
这一行加锁的代码上;
使用list
命令查看上下文代码,可以看见加锁了两次;
使用p g_mutex1
命令打印锁的信息可以看见__lock = 2
也是加锁了两次。
#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();
});
}
编译运行Qt程序后,点击pushButton按键,进入QtConcurrent线程,触发死锁;
使用ps -aux | grep 'testMutex\|USER'
命令查看死锁进程pid;
使用sudo gdb -q -p 21714
命令将gdb附加到进程;
使用info threads
命令查看所有线程的信息;
如下图所示,可看出线程7的类型为Thread(pooled)
(如果是使用QThread创建的线程这里类型就是QThread),这是使用线程池创建的QtConcurren线程,停止的堆栈帧的状态为syscall()
;程序停在syscall()
函数通常意味着它正在进行系统调用,而如果出现死锁后线程就会一直处于这种状态;
使用thread 7
命令切换到线程7;
使用bt
命令查看线程7堆栈信息;
如下图所示,利用看出QBasicMutex::lockInternal()
或者QMutex::lock()
,表示线程7堆栈停止在互斥锁的lock()函数位置,如何找到包含自己源代码的堆栈帧,在widget.cpp文件的29行。
使用f 3
命令切换到堆栈的第3帧,可以看的这一帧停止在g_mutex.lock()位置,正在加锁位置;
使用list
命令查看上下文代码,可以看出加锁两次;
使用p g_mutex
命令打印g_mutex锁的信息,和c++中的mutex锁不同,QMutex锁打印无法获得有帮助的信息。
使用代码和linux下一样。
进入到源代码所在路径;
使用g++.exe main.cpp -g -lpthread
命令编译代码(如果提升找不到g++则使用MinGw所在绝对路径);
执行a.exe
程序,触发死锁;
再打开一个cmd窗口;
使用gdb -q -p 8740
将gdb附加到进程调试;
使用info threads
命令查看所有线程信息(和linux下不同,不能直接看出死锁线程);
使用thread apply all bt
查看所有线程的堆栈信息;
如下图所示可以看出在线程2中出现了pthread_mutex_lock(),表示这个线程的堆栈停止在上锁位置,所以出现死锁,再往下找发现死锁位置出现在main.cpp文件的第21行中,threadFun1()函数位置。
后面操作就可有可无了,并且和linux下没有什么区别;
使用代码和linux下的相同;
注意:Windows下使用MinGW编译程序,调试时选择的gdb版本应该和编译的g++版本相同,不能使用32位的gdb调试64位的程序,或者相反。
Qt编译运行程序后,触发死锁;
打开对应版本的MinGW的cmd终端;
使用任务管理器窗口死锁程序的pid进程号;
使用gdb -q -p pid
将gdb附加到死锁进程;
直接使用thread apply all bt
显示所有线程的堆栈信息;
可以看出线程3出现死锁,后续操作都是一样的。
不过MinGW中gdb调试有时会出现下列情况,无法进行调试,目前没找到问题;
/********************************************************************************
* 文件名: main.cpp
* 创建时间: 2023-10-25 10:57:54
* 开发者: MHF
* 邮箱: [email protected]
* 功能: 多线程死锁示例
*********************************************************************************/
#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;
}
使用MSVC编译器编译代码,运行并触发死锁;
打开WinDbg程序,(在C:\Program Files\Windows Kits\10\Debuggers\x64
路径下);
选择【File】->【Attach to Process】或者直接按快捷键F6;
然后选择By ID,找到死锁进程,然后点击【OK】;
然后输入~*k
命令查看所有线程的堆栈信息,如下所示出现std::_Mutex_base::lock
字样,可看出在线程1、2出现死锁;
然后选择【View】,打开【Processes and Threads】窗口和【Calls Stack】窗口;
点击【Processes and Threads】窗口中的线程1,再点击【Calls Stack】窗口中的堆栈帧,就可以跳转到出现死锁的源码位置;
使用代码和Linux下的相同;
前面步骤都是相同的;
在使用~*k
命令窗口所有线程的堆栈信息时会发现看不到太多有帮助的信息,这时可用找包含源码文件的堆栈帧;
{__/}
(̷ ̷´̷ ̷^̷ ̷`̷)̷◞~❤
| ⫘ |