【Qt线程-4】事件循环嵌套,BlockingQueuedConnection与QWaitCondition比较

2023-05-16

背景:

个人学习多线程控制,写了一些博文用于记录:

【Qt线程-1】this,volatile,exec(),moveToThread()

【Qt线程-2】事件循环(QCoreApplication::processEvents,exec)的应用

【Qt线程-3】使用事件循环,信号,stop变量,sleep阻塞,QWaitCondition+QMutex条件变量,退出子线程工作

【Qt线程-5】生产者&消费者模型应用(多态,子线程控制,协同,事件循环)

主要用于线程间通信,实现子线程控制。

这里我想特别说明,事件循环是非常重要的概念,可以不知道原理,但是它的作用一定要理解透彻!!!它与操作系统的调度时序有关,与信号槽和队列响应有关,与deletelater有关,与QThread::wait()有关。这是目前为止我遇到的和理解的。

偶然看到一位朋友的博客:

14.QueuedConnection和BlockingQueuedConnection连接方式源码分析_Master Cui的博客-CSDN博客_blockingqueuedconnection

他很了不起,写了很多有深度的文章。从这篇博客中,原本很常用的信号槽机制,我突然想研究一下阻塞队列连接方式的效果。亦即:Qt::BlockingQueuedConnection,connect函数的最后一个参数。

概念——信号槽连接方式:

qt信号槽的连接方式一共就这几种:

Qt::AutoConnection

Qt::DirectConnection

Qt::QueuedConnection

Qt::BlockingQueuedConnection

Qt::UniqueConnection

qt手册以及网络上的介绍太多太详细了。其实很简单,通常我不用这个参数,直接就是auto模式,qt会自动布置。

信号与槽在同一个线程时,默认是direct方式,也就是顺序执行的,发送信号以后立刻执行槽函数,然后执行发信号后面的语句。发信号就好像调用函数一样,执行完函数再执行后面的代码。

信号与槽不再同一个线程时,默认是队列模式,执行顺序受os调度影响,异步执行。发送信号以后不会立刻执行另一个线程的槽函数,而是把发送信号后面的语句一口气执行完,再执行另一个线程的槽。这里涉及事件循环的概念,不再赘述。

同样是信号与槽不再一个线程,还可以采用阻塞队列模式,就是同时具备上两种方式的特点。发送完信号,直接执行槽,然后执行发信号后面的代码。发信号这个线程会阻塞一下,等待槽执行完毕。

最后一种见名知意,不允许多次连接,如果不选这种模式,连接一次,就会触发一次槽。

原本上面这些连接方式不需要特别深入,知道即可。但是我对阻塞队列模式感兴趣,因为它特别像条件变量的效果,QWaitCondition。所以,先看一种场景,事件循环嵌套。

场景——事件循环嵌套:

先看一段简单的代码:

#include "obj.h"
#include <QThread>
#include <QCoreApplication>
#include <QDebug>

Obj::Obj()
{

}

void Obj::f_Start()
{
    m_bStop = false;
    m_iLoopLevel++;

    while (true)
    {
        QCoreApplication::processEvents();//事件循环

        if (m_bStop)//退出判断
        {
            break;
        }

        QThread::msleep(200);//个人感觉,实际项目中少用sleep,用非阻塞方式友好一些
    }

    qDebug() << "Sub thread: event loop exists level: " << m_iLoopLevel;
    m_iLoopLevel--;

    if (m_iLoopLevel == 0)
    {
        f_Caller_Wakeup();
        qDebug() << "Sub thread: end.";
    }
}

void Obj::onStart()//这是一个槽函数,如果多次被触发,会让上面while循环中的eventloop嵌套
{
    f_Start();
}

void Obj::onStop()//用于接受停止命令,从而退出while循环,结束子线程
{
    qDebug() << "Sub thread: stopping...";
    m_bStop = true;
}

同时我做了一个窗体,用于人为控制子线程。

 点一次start就触发一层事件循环,点击stop会终止子线程所有循环。

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "obj.h"
#include <QThread>
#include <QDebug>
#include <QTextCodec>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    m_obj = new Obj;
    connect(this, SIGNAL(sigStart()), m_obj, SLOT(onStart()));
    connect(this, SIGNAL(sigStop()), m_obj, SLOT(onStop()));
    m_obj->setParent(nullptr);

    QThread *thd = new QThread;
    thd->start();

    m_obj->moveToThread(thd);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_btnStart_clicked()
{
    emit sigStart();
}

void MainWindow::on_btnStop_clicked()
{
    qDebug() << "Main thread send signal";
    emit sigStop();
    qDebug() << "Main thread end.";
}

目的——期望结果:

现在开始玩上面的代码,先说理想情况,我希望多次点击start产生循环嵌套以后,点击stop时,子线程逐级退出循环,而且主线程能正确获得已经结束的时机,从而再执行清理代码的时候不会造成野循环。

按照上面代码输出debug文本,我希望是这个顺序:

Main thread send signal.//主线程发信号
Sub thread: stopping...//子线程开始自我终结
Sub thread: event loop exists level:*//可能会有多级
Sub thread: end.//子线程结束
Main thread end.//主线程结束

上面只是理想,如果就按照上面的代码执行,效果不会实现,槽的执行方式默认时队列方式,它会先把主线程执行完,再执行子线程,这样在主线程写清理代码的时机就很重要,如果操作不当,会造成子线程野循环,报错。

Main thread send signal.//主线程发信号
Main thread end.//主线程结束
Sub thread: stopping...//子线程开始终结
Sub thread: event loop exists level:  3
Sub thread: event loop exists level:  2
Sub thread: event loop exists level:  1
Sub thread: end.//子线程结束

就如上面这样,显然不是想要的。如果一定要这样用,在子线程结束时还要给主线程发信号来触发清理操作。

应用——意义:

之所以要这样做,是希望主线程能够在终结子线程操作的时候能够保持时序,终结子线程行为之后,可以马上执行清理操作,而不会报错。主线程有一个“等待子线程完成”的动作。

如果不这样,发完信号就清理,子线程还没来得及停止所有操作,就已经被释放内存,内存倒是没泄露,可是正在执行的循环没有跳出,就是野循环,早晚会耗尽资源卡死,甚至直接报错。

所以有下面几种处理方式。

方式——阻塞连接:

所谓阻塞连接,就是信号槽的connect函数的最后一个参数使用Qt::BlockingQueuedConnection。按照阻塞连接的方式改一下主线程代码:

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "obj.h"
#include <QThread>
#include <QDebug>
#include <QTextCodec>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    m_obj = new Obj;
    connect(this, SIGNAL(sigStart()), m_obj, SLOT(onStart()));

    //使用阻塞连接方式,就改这一个地方,只是加了一个参数
    connect(this, SIGNAL(sigStop()), m_obj, SLOT(onStop()), Qt::BlockingQueuedConnection);

    m_obj->setParent(nullptr);

    QThread *thd = new QThread;
    thd->start();

    m_obj->moveToThread(thd);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_btnStart_clicked()
{
    emit sigStart();
}

void MainWindow::on_btnStop_clicked()
{
    qDebug() << "Main thread send signal.";
    emit sigStop();
    qDebug() << "Main thread end.";
}

只在connect的时候加了一个参数而已,再看效果。

Main thread send signal.//主线程发信号
Sub thread: stopping...//子线程执行槽,执行完槽回到eventloop
Main thread end.//主线程继续结束
Sub thread: event loop exists level:  3//子线程退出逐级循环
Sub thread: event loop exists level:  2
Sub thread: event loop exists level:  1
Sub thread: end.//子线程结束

看效果有点那个意思,但是子线程一旦执行完槽函数,它就回到自己最近的一层事件循环,这就算处理完消息队列了,所以接着会执行主线程的代码。因此还无法实现最初的想法。所以就有了使用条件变量的方法。

这里要说明,因为本篇文章我以事件循环嵌套为基础讨论,所以看不出阻塞队列方式的优点。事实上,如果子线程中只有槽函数需要主线程等待,那么使用阻塞队列方式,更加简洁高效。

方式——条件变量:

子线程对象定义,加入条件变量和响应的锁:

#ifndef OBJ_H
#define OBJ_H

#include <QObject>
#include <QWaitCondition>
#include <QMutex>

class Obj : public QObject
{
    Q_OBJECT
public:
    Obj();

    void f_Caller_Wait_Prepare();//用于主线程阻塞时获得锁
    void f_Caller_Wait();//用于主线程阻塞
    void f_Caller_Wakeup();//用于主线程唤醒

private slots:
    void onStart();
    void onStop();

private:
    QWaitCondition      m_condition;//条件变量
    QMutex              m_mutex;//互斥锁
    bool m_bStop = true;
    int m_iLoopLevel = 0;
    void f_Start();
};

#endif // OBJ_H

子线程实现:

#include "obj.h"
#include <QThread>
#include <QCoreApplication>
#include <QDebug>

Obj::Obj()
{

}

void Obj::f_Start()
{
    m_bStop = false;
    m_iLoopLevel++;

    while (true)
    {
        QCoreApplication::processEvents();

        if (m_bStop)
        {
            break;
        }

        QThread::msleep(200);
    }

    qDebug() << "Sub thread: event loop exists level: " << m_iLoopLevel;
    m_iLoopLevel--;

    if (m_iLoopLevel == 0)
    {
        f_Caller_Wakeup();//退出最后一层循环后,唤醒主线程
        qDebug() << "Sub thread: end.";
    }
}

void Obj::onStart()
{
    f_Start();
}

void Obj::onStop()
{
    qDebug() << "Sub thread: stopping...";
    m_bStop = true;
}

void Obj::f_Caller_Wait_Prepare()
{
    m_mutex.lock();
}

void Obj::f_Caller_Wait()
{
    qDebug() << "Main thread wait...";
    m_condition.wait(&m_mutex);
    m_mutex.unlock();
}

void Obj::f_Caller_Wakeup()
{
    qDebug() << "Main thread continue.";
    m_mutex.lock();
    m_condition.wakeAll();
    m_mutex.unlock();
}

主线程实现:

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "obj.h"
#include <QThread>
#include <QDebug>
#include <QTextCodec>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    m_obj = new Obj;
    connect(this, SIGNAL(sigStart()), m_obj, SLOT(onStart()));
    connect(this, SIGNAL(sigStop()), m_obj, SLOT(onStop()));//使用默认连接方式
    m_obj->setParent(nullptr);

    QThread *thd = new QThread;
    thd->start();

    m_obj->moveToThread(thd);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_btnStart_clicked()
{
    emit sigStart();
}

void MainWindow::on_btnStop_clicked()
{
    qDebug() << "Main thread send signal.";
    m_obj->f_Caller_Wait_Prepare();//获得锁,保证emit之后马上wait,不会被调度插队
    emit sigStop();
    m_obj->f_Caller_Wait();//主线程等待
    qDebug() << "Main thread end.";
}

执行效果如下:

Main thread send signal.//主线程发信号
Main thread wait...//主线程等待
Sub thread: stopping...//子线程开始终结
Sub thread: event loop exists level:  6
Sub thread: event loop exists level:  5
Sub thread: event loop exists level:  4
Sub thread: event loop exists level:  3
Sub thread: event loop exists level:  2
Sub thread: event loop exists level:  1
Main thread continue.//唤醒主线程
Sub thread: end.//子线程终结
Main thread end.//主线程终结

这就实现最初的想法了。

方式——子线程回复信号:

主线程发完信号之后不要马上清理,子线程完成后发信号通知主线程已经结束,再清理。下面试试这种方式。

子线程加一个sigStopped信号,发给主线程。主要看一下实现代码。

子线程实现:

#include "obj.h"
#include <QThread>
#include <QCoreApplication>
#include <QDebug>

Obj::Obj()
{

}

void Obj::f_Start()
{
    m_bStop = false;
    m_iLoopLevel++;

    while (true)
    {
        QCoreApplication::processEvents();

        if (m_bStop)
        {
            break;
        }

        QThread::msleep(200);
    }

    qDebug() << "Sub thread: event loop exists level: " << m_iLoopLevel;
    m_iLoopLevel--;

    if (m_iLoopLevel == 0)
    {
        qDebug() << "Sub thread: end.";
        emit sigStopped();//退出最后一层循环后,通知主线程
    }
}

void Obj::onStart()
{
    f_Start();
}

void Obj::onStop()
{
    qDebug() << "Sub thread: stopping...";
    m_bStop = true;
}

主线程实现:

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "obj.h"
#include <QThread>
#include <QDebug>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    m_obj = new Obj;
    connect(this, SIGNAL(sigStart()), m_obj, SLOT(onStart()));
    connect(this, SIGNAL(sigStop()), m_obj, SLOT(onStop()));
    connect(m_obj, SIGNAL(sigStopped()), this, SLOT(onSubStopped()));//增加一个回复连接
    m_obj->setParent(nullptr);

    QThread *thd = new QThread;
    thd->start();

    m_obj->moveToThread(thd);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_btnStart_clicked()
{
    emit sigStart();
}

void MainWindow::on_btnStop_clicked()
{
    qDebug() << "Main thread send signal.";
    emit sigStop();//发送完信号什么也不做,等待子线程回复
}

void MainWindow::onSubStopped()//子线程发回已终止信号之后被触发
{
    qDebug() << "Main thread end.";
}

执行效果:

Main thread send signal.//主线程发信号
Sub thread: stopping...//子线程开始自我终结
Sub thread: event loop exists level:  3
Sub thread: event loop exists level:  2
Sub thread: event loop exists level:  1
Sub thread: end.//子线程终止
Main thread end.//主线程终止

看效果还是不错的。

看起来这种完全异步方式更灵活,但是看情况,太多的异步交互不太容易阅读代码。

毕竟是异步的,多线程执行受os调度影响,一定要想清楚时序。

结论:

所以我个人看法是:

如果子线程需要父线程等待执行的,只有子线程的槽函数。就用阻塞连接方式,只是加一个参数,简洁高效。

如果子线程工作的终止可能单方面发生, 让子线程回复一个完成信号的方式更加灵活机动,完全异步实现,消耗资源也少。比如:网络通信中的客户端,它可能由于自己的原因掉线了,就需要主动通知服务端。

如果是纯粹的主从模式,子线程的终止操作,只受命于父线程。亦即,主线程对于子线程有绝对控制权的时候,可以使用条件变量。因为主线程需要主动有“等”这个动作。条件变量可以严格控制时序。手动控制它什么时候等待,什么时候被唤醒,确保时序万无一失。比如:控制器和执行器之间。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【Qt线程-4】事件循环嵌套,BlockingQueuedConnection与QWaitCondition比较 的相关文章

随机推荐

  • 记一次在Taro开发的微信小程序中使用lottie动画的经验

    前景提要 最近在做公司项目的时候 xff0c 看到移动端开发用的小图标有动态效果 xff0c 非常好玩了解到是使用lottie进行实现的 xff0c 这个东西以前有看到过对应的插件库 xff0c 但是一直没有时间做研究 xff0c 趁着这个
  • JDB调试Android程序(通过JDB进行代码注入)

    前言 最近在做一些安卓安全相关的事情 xff0c 就看到了一个通过动态调试进行代码注入的一个概念 xff0c 收益匪浅 xff0c 原来好多东西还能这么玩的 闲言少絮 xff0c 开始正式行动 漏洞检查 由于我这边是做的关于安卓安全相关的事
  • [2019.12.20]strncpy发生stack corruption detected(-fstack-protector)栈溢出

    代码 char line MAX 61 0 strncpy line pBeginObj ptemp pBeginObj 43 1 log如下 解释 char strncpy char dest const char src int n 把
  • libmng.so.1: cannot open shared object file: No such file or directory

    span class token function sudo span span class token function ln span s usr lib x86 64 linux gnu libmng so 2 usr lib x86
  • 企业级数据模型主题域模型划分( IBM-FSDM)

    一 前言 如何构建主题域模型原则是构建企业级数据仓库重要的议题 xff0c 最好的路径就是参照成熟的体系 IBM金融数据模型数据存储模型FSDM xff0c 是金融行业应用极为广泛的数据模型 xff0c 可以作为我们构建企业级数据仓库主题域
  • 关于编程学习上的一些感悟——不忘初心

    序 今天无意中看到以前一起开发过的同学写的技术文章 xff0c 了解到了更多在blog和github以及一些技术交流论坛上面非常活跃 回过头来看看自己 xff0c 好像依然停留在以前的样子 xff0c 似乎与真正在踏实学技术差距好像很大了
  • CentOS下ns-3安装教程

    首先 xff0c 安装ns 3时最好不要使用root权限 xff0c 普通用户安装即可 xff0c 否则后来要找文件会比较麻烦 一 安装依赖软件包 首先安装依赖软件包 根据官网 xff08 https www nsnam org wiki
  • 生产者-消费者模型

    文章来自https github com NieJianJian AndroidNotes xff0c 内容将持续更新 xff0c 欢迎star 一 前言 生产者消费者模式并不是GOF提出的23种设计模式之一 xff0c 23种设计模式都是
  • JAVA 多线程解决高并发、超时线程池耗尽问题

    第一类 问题 项目中遇到了 创建20个固定线程的线程池 在测试环境 多线程如果高并发的调用都没出现问题 但是在实际的项目中 出现了线程池内线程超时等待并将池内的线程耗尽 导致其它的程序走到多线程调用时候出现了执行慢 线程无法执行问题 问题原
  • 31_谈谈你对线程安全的理解?(重点)

    如果这个是面试官直接问你的问题 xff0c 你会怎么回答 xff1f 一个专业的描述是 xff0c 当多个线程访问一个对象时 xff0c 如果不用进行额外的同步控制或其他的协调操作 xff0c 调用这个对象的行为都可以获得正确的结果 xff
  • MariaDB 数据类型

    MariaDB 数据类型 数字数据类型 MariaDB支持的数字数据类型如下 类型描述TINYINT此数据类型表示落入 128到127的有符号范围内的小整数 xff0c 以及0到255的无符号范围 BOOLEAN此数据类型将值0与 fals
  • DBSCAN算法(python代码实现)

    DBSCAN 上次学了kmeans基于划分的方法 xff0c 这次学一个基于密度的聚类算法 xff1a DBSCAN xff08 Density Based Spatial Clustering of Applications with N
  • vs2022(缺少MFC,无法新建项目,控件无法添加事件)的解决

    最近下载安装了最新的vs2022社区版 xff0c 想着把之前的c 43 43 项目能够兼容 xff0c 于是遇到了一些列问题 缺少MFC xff0c 无法新建项目 xff0c 控件无法添加事件 这里首先要吐槽一下 xff1a 也许是我电脑
  • C#多线程加载控件界面卡死的解决

    先听一个故事 xff1a 有一个老板忙不过来 xff0c 于是招一个员工去负责某些事务 这样老板就可以腾出时间处理其它事 后来发现员工干不下去 xff0c 原因是干活需要花费 xff0c 没有老板的认可 xff0c 财务不给批钱 这是原则
  • vs2022账户无法登录的解决

    因为昨天重做系统 xff0c 重装了vs2022 xff0c 又涉及到登录的问题 xff0c 一时想不起来之前怎么解决的了 xff0c 想起来以后决定还是记录下来 我遇到的问题是下面这样的 xff0c 提示脚本错误 xff0c 要求升级最新
  • 使用centos7+bind9构建内网私有dns

    有这样一种场景 xff0c 局域网内有一个为网内用户提供服务的机器 xff0c 我们希望像访问互联网站点一样去访问它 xff0c 而不用记忆ip地址和端口 xff0c 比如在web浏览器地址栏输入http www nx com就可以访问它
  • PowerBuilder制作纸牌游戏

    本文记录的是2001年我大三那年假期制作小游戏的思路 xff0c 希望给在读计算机专业的朋友们一些参考 xff0c 如果还没来得及动手尝试的同学 xff0c 一定要勇于动手 你们一定比我做得更好 制作动力 xff1a 我有一个好朋友 xff
  • 【Qt线程-1】this,volatile,exec(),moveToThread()

    背景 xff1a 个人学习多线程控制 xff0c 写了一些博文用于记录 xff1a Qt线程 2 事件循环 xff08 QCoreApplication processEvents xff0c exec xff09 的应用 Qt线程 3 使
  • 【Qt线程-2】事件循环(QCoreApplication::processEvents,exec)的应用

    背景 xff1a 个人学习多线程控制 xff0c 写了一些博文用于记录 Qt线程 1 this xff0c volatile xff0c exec xff0c moveToThread Qt线程 3 使用事件循环 xff0c 信号 xff0
  • 【Qt线程-4】事件循环嵌套,BlockingQueuedConnection与QWaitCondition比较

    背景 xff1a 个人学习多线程控制 xff0c 写了一些博文用于记录 xff1a Qt线程 1 this xff0c volatile xff0c exec xff0c moveToThread Qt线程 2 事件循环 xff08 QCo