栏目分类:
子分类:
返回
文库吧用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
文库吧 > IT > 软件开发 > 后端开发 > C/C++/C#

Qt事件循环(QCoreApplication::processEvents,exec)的应用

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Qt事件循环(QCoreApplication::processEvents,exec)的应用

前言:

个人笔记,如有异议,欢迎讨论。

以前用vb或者c#的时候,需要响应哪个事件,就可以添加响应函数来做相应处理。vc++的mfc也是如此。但是它内部是如何实现的,即时暂时不知道,大多数情况下也没有影响做项目。但是在qt中,消息和槽成为了比较通用的概念,很多时候更善于利用它进行参数传递。但事实上,消息和槽机制,离不开事件循环。

概念:

所谓事件循环,就是循环执行消息响应,此时一旦消息队列有消息发生,就可以马上执行槽响应函数。

场景——main函数:

就如常见的main函数,主窗体打开后,后面有个exec(),这就是事件循环。可以理解为一个死循环,永远在等待消息队列。所以,在窗体中放个控件,才可以有槽函数来处理信号响应。

#include "mainwindow.h"
#include 

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();//事件循环
}

如果没有它,主窗体会闪一下就过去了。比如这样:

#include "mainwindow.h"
#include 

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return 0;
    //return a.exec();
}

像这样main函数以很快的速度创建一个窗体并显示一下,然后又顺理成章地返回0,主进程随着main函数的结束而结束,窗体只是闪一下,什么都干不了。

场景——run函数:

写多线程的时候,可以从QThread派生一个线程类,而且要重写run函数。重写run函数的时候,底部要加一个exec()事件循环。

void MyThread::run()
{
    //希望在线程中完成的操作...

    exec();//事件循环
}

跟main函数一样,这里必须写事件循环,否则run函数执行完前面代码之后会直接结束,线程就结束了。

这里穿插一个概念,所谓线程,不是new了一个线程对象就是线程,这个线程对象其实是在父线程中,跟其它对象一样,new了一个实例而已。它仅仅存在于父线程,它可以作为控制线程的句柄。而真正的线程过程,是run函数启动以后,写在run函数中的代码。所以使用继承QThread并重写run函数的方式实现线程时,一定切记,不是所有函数就一定会在线程中执行,除非它被run函数调用,或者在run当中使用rambda写匿名槽函数。而写匿名槽函数的时候,接收者千万别写this,this指针是指向父线程的线程对象,能作为句柄控制线程,但this隶属于父线程。所以一旦写了this是接收者,这个匿名槽函数会在父线程执行。而子线程中创建对象时,也不要指定parent为this。

所以,在run函数中写exec,它会阻止run函数结束,让子线程始终等待消息队列的任务,从而实现利用信号槽进行线程通信。

场景——子线程对象

上面说过线程的实现,离不开父线程的线程对象,它仅仅是子线程的操作句柄。如果一定要让一个槽函数运行于子线程中,可能还少不了要写个对象再使用movetothread让它进入线程。所以,个人认为,写线程的时候,更好的方法是不要继承QThread并重写run函数。而是把要执行的逻辑写成一个类,实例化以后movetothread,可以确保它一定是在子线程中运行。

MyObject *obj = new MyObject;
QThread *thd = new QThread;
obj->setParent(NULL);
obj->moveToThread(thd);
thd->start();

如果是不太繁忙的工作,可能不需要考虑下面的问题。但我写了一个模拟生产者的逻辑,使用了while循环,这中间需要写sleep来让出cpu资源。比如下面代码中有个变量m_bStop作为标记用来停止工作流程,但是这个变量什么时候生效?就要sleep之后,它才有机会被外部线程修改并生效。

while (!m_bStop)
{
    //Do something

    //这是必须的,否则当前线程无休止循环,根本没有机会让m_bStop变量被外部更改。
    //可以用qDebug在sleep前后分别输出一下m_bStop的值,立见分晓。
    QThread::msleep(500);
}

有个m_bStop变量用于控制退出循环从而结束工作流程。如果把m_bStop声明成public volatile,这样从线程外面可以控制线程中的流程什么时候结束。

但如果有些参数需要调整,其实也可以用这种方式。但如果想使用信号和槽,上面的写法行不通的。虽然sleep出让了cpu资源,它只是阻塞当前线程,并不会让当前线程“休息”来等待响应信号。所以这里还要用事件循环。于是就有了下面的写法。

while (!m_bStopAll)
{
        //do something.
       
        QCoreApplication::processEvents();

        //Release the cup resource a moment.
        QThread::sleep(m_iInterval);
}

说实话,当第一次用这个时,着实费脑子有些场景可能会想不通。如果类似调整参数或者收发消息之类的操作,应该是没问题的。但是这个子线程我是使用一个子窗体启动它的。而我希望一旦用户中途关闭子窗体,这个子线程也应该自动停止并关闭。

所以我在子窗体中加了这些:

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

    //构造函数中加这么一句,或者写在初始化函数中,目的是关闭窗体时候,
    //调用析构函数,因为默认是不调用的。多次踩坑。
    setAttribute(Qt::WA_DeleteOnClose);
}

FrmProducer::~FrmProducer()
{
    //问题一:
    //最初这里使用信号方式通知子线程结束,事实证明不可取。
    //所以直接使用控制变量来作为while循环的跳出条件。
    m_producer->m_bStopAll = true;

    //问题二:
    //释放子线程中的对象资源
    if (nullptr != m_producer)
    {
        m_producer->deleteLater();//in event loop
        m_producer = nullptr;
    }

    //关闭线程并释放线程资源
    if (nullptr != m_thd && m_thd->isRunning())
    {
        m_thd->quit();
        m_thd->wait();
        m_thd->deleteLater();//in event loop
        m_thd = nullptr;
    }

    delete ui;
}

上面的简化代码,大致说明意思即可。

其中释放资源的部分是需要思维清晰并理解深入的。

问题一:

为了通知子线程结束流程,其实最先想到的是发送信号。但线程间通信,信号槽是异步的,亦即发送信号后,不是子线程马上执行槽函数,而是等待本线程先执行完,回到事件循环之后,子线程再执行槽函数。这有个调度顺序问题。

所以一定要考虑清除,发完信号之后的代码,千万不要提前断了子线程的后路,否则子线程的逻辑必然报错。

问题二:

上面说了要注意发完信号之后的代码,所以我用了deletelater延迟销毁。但这个函数的含义是,等待子线程执行完逻辑并回到它的事件循环后,再执行销毁。这里不注意就尴尬了。

比如之前的代码:

while (!m_bStopAll)
{
        //do something.
       
        //这里是事件循环,也就是执行到这里,父线程的deletelater会生效,
        //然后子线程的整个世界就毁灭了,所有操作必须安全退出,否则必然报错。
        QCoreApplication::processEvents();

        //出让cpu资源,顺便给m_bStopAll这种标记变量一个被外部更改的机会。
        QThread::sleep(m_iInterval);
}

既然事件循环用于信号和槽,也用于deletelater。那它是先执行结束while循环的响应还是deletelater呢?事件循环的说明是它会执行完队列中的所有该响应的信号。所以分析一下过程:

如果发送停止信号给子线程,然后马上写了deletelater来清理资源。它执行到事件循环时一定会先响应停止信号,并赋予标记变量m_bStop=true。然后马上清理资源。这就真尴尬了。

因为设置m_bStop=true是希望下一轮while时被检测然后退出while循环。但事实上还不等到下一轮,deletelater就被执行,子线程的世界在while循环停止前提前崩塌了,所以while依然会执行,只是之前的标记变量已经随着子线程的销毁而销毁,它的值已经不准了,所以极有可能造成无人看守的“野循环”。就好像野指针,这显然是不行的。

解决:

所以我上面的方法是:

FrmProducer::~FrmProducer()
{
    //直接使用控制变量来作为while循环的跳出条件。
    m_producer->m_bStopAll = true;
  
    //释放子线程中的对象资源
    if (nullptr != m_producer)
    {
        m_producer->deleteLater();//in event loop
        m_producer = nullptr;
    }

    //关闭线程并释放线程资源
    if (nullptr != m_thd && m_thd->isRunning())
    {
        m_thd->quit();
        m_thd->wait();
        m_thd->deleteLater();//in event loop
        m_thd = nullptr;
    }

    delete ui;
}

子线程也要调整:

while (!m_bStopAll)
{       
    //在事件循环之前检测m_bStopAll这种标记变量,
    //这时候它不会因为deletelater销毁子线程后而无效

    //事件循环用于响应信号和deletelater
    QCoreApplication::processEvents();

    //耗时工作放在这里。

    //出让cpu资源,顺便给m_bStopAll这种标记变量一个被外部更改的机会。
    QThread::sleep(m_iInterval);
}

这就没问题了。用于跳出while循环的标记变量,一定要在生效的时候去判断,所以一定要写在事件循环之前。

而这种标记变量的值,一定是在sleep的时候才有机会被外部更改。

所以上面的while的结束过程是这样的:

之前N轮循环完成工作。

sleep后外部改变m_bStop的值,标记生效。

下一轮开始while(m_bStop),循环退出。

--------------------------

然后就没有然后了,它内部写的那句事件循环
QCoreApplication::processEvents();
已经没机会执行。

因为while循环跳出后,子线程对象执行完毕,
回到了QThread的run函数中默认的exec()事件循环。
所以依然会执行资源清理:

    if (nullptr != m_worker)
    {
        m_worker->deleteLater();//in event loop
        m_worker = nullptr;
    }

    if (nullptr != m_thd && m_thd->isRunning())
    {
        m_thd->quit();
        m_thd->wait();
        m_thd->deleteLater();//in event loop
        m_thd = nullptr;
    }

从而,子线程工作对象中的QCoreApplication::processEvents();
已经不能导致deletelater在不恰当的时机发生。

所以,本人连续踩坑之后,发现还是自己理解不够深入,要说不懂也不是,但缺乏推敲,真的认真看了说明,也就想通了。

转载请注明:文章转载自 www.wk8.com.cn
本文地址:https://www.wk8.com.cn/it/1037210.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 wk8.com.cn

ICP备案号:晋ICP备2021003244-6号