Qt案例 滥用[Qt::BlockingQueuedConnection]队列链接导致出现程序死锁Bug的问题
记录项目开发过程中,使用QThread线程连接信号时,频繁使用Qt::BlockingQueuedConnection 队列连接造成的程序死锁。以及还原类机构解决问题过程。
目录导读
- 方向一:bug问题描述
- 方向二:bug解决过程
- 还原造成死锁bug异常的类结构:
- 创建 Dal_DownData 下载类
- 创建 QThread_Down 线程基类
- 创建 QThread_Operation 线程类
- 还原造成死锁bug异常的具体操作:
- 使用回调函数解决死锁问题
- 方向三:bug经验教训
方向一:bug问题描述
就在最近的开发过程中,遇到一个需要在线程中先下载数据,再对下载的数据进行一系列处理的需求,
在实际开发中,我创建了一个下载类 Dal_DownData 用来下载服务器数据,
创建了一个线程基类 QThread_Down 用来处理下载和实现一系列方法。
又创建了一个线程类 QThread_Operation 继承 QThread_Down 类 用来选择实际需要执行的方法。
实际流程就是Dal_DownData 下载类 把下载的进度信号传递给 线程基类 QThread_Down ,用 线程类 QThread_Operation 与界面绑定的进度条信号。然而在实际使用过程中出现了
Qt: Dead lock detected while activating a BlockingQueuedConnection
的异常BUG,导致软件直接崩溃。
查看QBreakpad监控异常,发现没有生成任何异常dmp文件…
当时也没发现是Qt::BlockingQueuedConnection 队列连接的问题,
于是在网上找各种资料,耗时一天半,
最后用回调函数传递信号的方式解决了…
最初我以为是
下载类传达信号给线程类中转信号的时候造成的线程死锁,,觉得这个例子比较典型在准备抽空还原这个Bug异常参与话题写个随笔的时候,通过还原类的结构发现,只需要不使用Qt::BlockingQueuedConnection 队列连接就不会有任何问题…
那一瞬间我真的是我有橘麻麦皮不知当浆不当浆。
方向二:bug解决过程
还原造成死锁bug异常的类结构:
Dal_DownData.h:
//! 定义一个下载类 伪代码
class Dal_DownData:public QObject
{
Q_OBJECT
public:
Dal_DownData();
void StartDown(QString file="text");
Q_SIGNALS:
///开始下载
void IsStart(bool bol);
/// 直接进度条设置业务值(百分值)
void ProgressBar(int value);
/// 直接进度条设置业务值(百分值)
void StyleStr(QString type);
};
Dal_DownData.h:
#include <QDebug>
Dal_DownData::Dal_DownData()
{
}
void Dal_DownData::StartDown(QString file)
{
emit IsStart(true);
for(int i=1;i<=100;i++)
{
//! 作为下载的一个进度效果
emit ProgressBar(i);
if(PROGRESSBAR!=nullptr)
PROGRESSBAR(i);
emit StyleStr("ACTIVE");
// std::this_thread::sleep_for(std::chrono::seconds(1));
Sleep(500);
}
emit IsStart(false);
}
QThread_Down.h
//! 一个下载线程
class QThread_Down:public QThread
{
Q_OBJECT
public:
QThread_Down(QObject* parent=nullptr);
//! 修改操作1
void Function1();
//! 修改操作2
void Function2();
Q_SIGNALS:
///开始
void IsStart(bool bol);
void SendMessStr(QString str);
protected:
Dal_DownData* Down=nullptr;
};
QThread_Down.cpp
QThread_Down::QThread_Down(QObject* parent)
:QThread(parent)
{
Down=new Dal_DownData();
connect(Down,&Dal_DownData::IsStart,this,[&](bool bol){
if(bol)
emit QThread_Down::SendMessStr("开始下载...");
else
emit QThread_Down::SendMessStr("下载结束...");
},Qt::BlockingQueuedConnection);
connect(Down,&Dal_DownData::ProgressBar,this,[&](int val){
emit QThread_Down::SendMessStr(QString("\r正在下载:[%1%]...").arg(val));
},Qt::BlockingQueuedConnection);
connect(Down,&Dal_DownData::StyleStr,this,[&](QString val){
emit QThread_Down::SendMessStr(QString("\r下载状态:[%1]...").arg(val));
},Qt::BlockingQueuedConnection);
}
void QThread_Down::Function1()
{
msleep(500);
}
void QThread_Down::Function2()
{
msleep(500);
}
线程类 QThread_Operation 继承 QThread_Down 线程类。
QThread_Operation.h
class QThread_Operation:public QThread_Down
{
Q_OBJECT
public:
QThread_Operation(QObject* parent=nullptr);
void run() override;
};
QThread_Operation.cpp
QThread_Operation::QThread_Operation(QObject* parent)
:QThread_Down(parent)
{
}
void QThread_Operation::run()
{
emit IsStart(true);
emit SendMessStr("开始线程...");
emit SendMessStr("等待三秒后开始调用下载...");
// std::this_thread::sleep_for(std::chrono::seconds(3));
msleep(500);
Down->StartDown();
emit SendMessStr("选择需要执行的操作等等...");
Function1();
// std::this_thread::sleep_for(std::chrono::seconds(1));
msleep(500);
emit SendMessStr("结束线程...");
emit IsStart(false);
}
还原造成死锁bug异常的具体操作:
在测试的时候我是使用的控制台程序输出结果:
最开始的时候,我正常连接信号,因为控制台程序没有this变量,所以就没有修改连接信号槽的方式。
QThread_Operation* operation=new QThread_Operation() ;
QObject::connect(operation,&QThread_Operation::IsStart,[&](bool bol){
if(bol)
qDebug()<<"线程启动!";
else
qDebug()<<"线程结束!";
});
QObject::connect(operation,&QThread_Operation::SendMessStr,[&](QString Str){
qDebug().noquote()<<Str;
});
operation->start();
结果正常输出结果,没有出现死锁,
我以为是我改用了 std::this_thread::sleep_for(std::chrono::seconds(3))
暂停线程的问题,于是改用MScv编译器使用 Sleep(500)
暂停线程。
结果输出结果依旧正常。
于是修改进度大小,添加中转的信号个数,依旧没有问题,
返回项目环境测试还是有死锁,不是偶发事件。继续修改测试
直到我为了与原版内容结构一致,添加了Qt::BlockingQueuedConnection
信号
//a是指QCoreApplication a(argc, argv);
QObject::connect(operation,&QThread_Operation::SendMessStr,&a,[&](QString Str){
qDebug().noquote()<<Str;
},Qt::BlockingQueuedConnection);
出现死锁:
Qt: Dead lock detected while activating a BlockingQueuedConnection: Sender is QThread_Operation(0x22b5ce12b90), receiver is QCoreApplication(0x9a027ef870)
有点疑惑,自认为对于Qt::BlockingQueuedConnection
连接方式我还是有点心得的,还相到还能出个岔子,直到我找到了以前文章中摘抄的一段关于信号连接类型的说明:
Qt::BlockingQueuedConnection:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。
出自:QT 面试题 个人标注重点
于是我移除Dal_DownData类与QThread_Down类绑定的中转信号的连接方式
Down=new Dal_DownData();
connect(Down,&Dal_DownData::IsStart,this,[&](bool bol){
if(bol)
emit QThread_Down::SendMessStr("开始下载...");
else
emit QThread_Down::SendMessStr("下载结束...");
});
connect(Down,&Dal_DownData::ProgressBar,this,[&](int val){
emit QThread_Down::SendMessStr(QString("\r正在下载:[%1%]...").arg(val));
});
connect(Down,&Dal_DownData::StyleStr,this,[&](QString val){
emit QThread_Down::SendMessStr(QString("\r下载状态:[%1]...").arg(val));
});
还是死锁,只有移除QThread_Operation线程类与控制台程序的 Qt::BlockingQueuedConnection 连接方式,才正常输出。
我估摸着信号的接收者和发送者也没在同一线程上,咋还成死锁了,
盲猜可能就是QThread_Operation线程堵塞的时候下载线程输出的信号还再传递,所以修改为通过 回调函数
的方式传递信号,线程堵塞时都被暂停了,这样即使再使用Qt::BlockingQueuedConnection 信号连接依旧正常输出.
使用回调函数解决死锁问题
在没有判断出 是Qt::BlockingQueuedConnection 连接方式造成的问题之前,我是通过定义回调函数的方法解决的这个问题,
这里做一个Qt开发中常使用的回调函数的调用示例:
回调函数简单示例:
//! 定义一个回调函数
typedef std::function<void(int)> CallbackFunction_ProgressBar;
auto _T=[this](int i){
emit QThread_Down::SendMessStr(QString("\r正在下载:[%1%]...").arg(i));
};
CallbackFunction_ProgressBar _ProgressBar =_T;
实际操作:
- 重写下载类 Dal_DownData
使用std::function
定义回调函数类型,
在传递信号的地方调用回调函数。
定义回调函数类型变量,伪代码。
Dal_DownData.h
#include <QObject>
#include <iostream>
#include <functional>
#include <thread>
#include <chrono>
#include <Windows.h>
//! 定义一个回调函数
typedef std::function<void(int)> CallbackFunction_ProgressBar;
typedef std::function<void(QString)> CallbackFunction_StyleStr;
typedef std::function<void(bool)> CallbackFunction_IsStart;
//! 定义一个下载类 伪代码
class Dal_DownData:public QObject
{
Q_OBJECT
public:
Dal_DownData();
void StartDown(QString file="text");
//! 设置信号的回调函数
void SetCallBack(CallbackFunction_ProgressBar _ProgressBar=nullptr,
CallbackFunction_StyleStr _StyleStr=nullptr,
CallbackFunction_IsStart _IsStart=nullptr)
{
PROGRESSBAR = _ProgressBar;
STYLESTR = _StyleStr;
ISSTART = _IsStart;
}
Q_SIGNALS:
///开始下载
void IsStart(bool bol);
/// 直接进度条设置业务值(百分值)
void ProgressBar(int value);
/// 直接进度条设置业务值(百分值)
void StyleStr(QString type);
private:
CallbackFunction_ProgressBar PROGRESSBAR=nullptr;
CallbackFunction_StyleStr STYLESTR=nullptr;
CallbackFunction_IsStart ISSTART=nullptr;
};
Dal_DownData.cpp
#include "dal_downdata.h"
#include <QDebug>
Dal_DownData::Dal_DownData()
{}
void Dal_DownData::StartDown(QString file)
{
emit IsStart(true);
if(ISSTART!=nullptr)
ISSTART(true);
for(int i=1;i<=100;i++)
{
//! 作为下载的一个进度效果
emit ProgressBar(i);
if(PROGRESSBAR!=nullptr)
PROGRESSBAR(i);
emit StyleStr("ACTIVE");
if(STYLESTR!=nullptr)
STYLESTR("ACTIVE");
// std::this_thread::sleep_for(std::chrono::seconds(1));
Sleep(500);
}
emit StyleStr("DOWNOVER");
if(STYLESTR!=nullptr)
STYLESTR("DOWNOVER");
emit IsStart(false);
if(ISSTART!=nullptr)
ISSTART(false);
}
修改在线程类中下载类的绑定方式;
其中可以通过std::bind
和lambda 表达式
绑定回调函数,通过方法赋值变量。如下示例:
Down=new Dal_DownData();
//! 之前通过信号信号槽的方式
// connect(Down,&Dal_DownData::IsStart,this,[&](bool bol){
// if(bol)
// emit QThread_Down::SendMessStr("开始下载...");
// else
// emit QThread_Down::SendMessStr("下载结束...");
// });
// connect(Down,&Dal_DownData::ProgressBar,this,[&](int val){
// emit QThread_Down::SendMessStr(QString("\r正在下载:[%1%]...").arg(val));
// });
// connect(Down,&Dal_DownData::StyleStr,this,[&](QString val){
// emit QThread_Down::SendMessStr(QString("\r下载状态:[%1]...").arg(val));
// });
//! 绑定回调函数两种方法
// 使用 std::bind 将 QThread_Down 的成员函数绑定到其实例上
auto _DownStyleStr = std::bind(&QThread_Down::DownStyleStr, this,std::placeholders::_1);
// 使用 lambda 表达式捕获 QThread_Down 的实例并调用其成员函数
auto _IsStart=[this](bool bol){
if(bol)
emit QThread_Down::SendMessStr("开始下载...");
else
emit QThread_Down::SendMessStr("下载结束...");
};
auto _ProgressBar=[&](int val){
emit QThread_Down::SendMessStr(QString("\r下载进度:[%1%]...").arg(val));
};
Down->SetCallBack(_ProgressBar,_DownStyleStr,_IsStart);
使用回调函数后,在使用Qt::BlockingQueuedConnection 连接也不会造死锁问题了…
方向三:bug经验教训
为什么要还要使用Qt::BlockingQueuedConnection 信号连接?
因为以前做大数据量处理时发现,多个线程同时向界面传达大量信号刷新界面,依然会造成界面卡顿,习惯性通过Qt::BlockingQueuedConnection 信号连接方式,牺牲部分数据处理速度同步界面刷新,防止界面卡顿。
而之前实际开发中,实际上默认的信号槽连接方式就已经可以了,但是因为开发习惯和不了解信号类型滥用,导致浪费大量时间处理造成的bug。
同时需要注意Qt::BlockingQueuedConnection 信号的使用不合理,可能会造成死锁!
慎用!
引以为戒阿…