Qt案例 滥用[Qt::BlockingQueuedConnection]队列链接导致出现程序死锁Bug的问题-方向二:bug解决过程

时间:2024-11-22 22:37:25

还原造成死锁bug异常的类结构:

  • 创建 Dal_DownData 下载类

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 线程基类

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_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 信号连接依旧正常输出.

回调函数简单示例:
//! 定义一个回调函数
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;