当前位置: 首页 > article >正文

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 下载类

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

使用回调函数解决死锁问题

在没有判断出 是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::bindlambda 表达式绑定回调函数,通过方法赋值变量。如下示例:

    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 信号的使用不合理,可能会造成死锁!
慎用!
引以为戒阿…


http://www.kler.cn/a/409094.html

相关文章:

  • Windows系统运行库软件游戏修复工具
  • 【MyBatis】全局配置文件—mybatis.xml 创建xml模板
  • (免费送源码)计算机毕业设计原创定制:Java+JSP+HTML+JQUERY+AJAX+MySQL springboot计算机类专业考研学习网站管理系统
  • 【操作系统】每日 3 题(三十五)
  • Redis——Raft算法
  • 利用c语言详细介绍下插入排序
  • JavaWeb开发:HTML 页面与接口对接
  • 基于Java Springboot工厂生产设备维护管理系统
  • Facebook商城号封号的原因是什么?
  • 数据集-目标检测系列- 花卉 玫瑰 检测数据集 rose >> DataBall
  • C/C++ 每日一练:实现字符串的大小写转换
  • unity3d————基础篇小项目(设置界面)
  • linux常见版本:
  • 本地部署 MaskGCT
  • 网络爬虫——综合实战项目:多平台房源信息采集与分析系统
  • Python爬虫:深度解析1688接口数据获取
  • 在线解析工具链接
  • 力扣题解3248 矩阵中的蛇(简单)
  • 什么是Sass,有什么特点
  • Leetcode 生命游戏
  • 文献阅读11.24
  • Linux 下进程基本概念与状态
  • Spring Boot应用开发实战:构建RESTful API服务
  • 10大核心应用场景,解锁AI检测系统的智能安全之道
  • 网络安全应急响应及其发展方向
  • SQL注入靶场演练