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

背景:

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

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

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

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

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

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

【Qt线程-6】获取当前线程id,thread()和currentThreadId(),不是想当然那样,不使用信号槽可能看不出区别

【Qt线程-7】QThread、QThreadPool+QRunnable怎么选

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

概念:

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

QCoreApplication::processEvents。qt手册这样说的:

Processes all pending events for the calling thread according to the specified flags until there are no more events to process.
You can call this function occasionally when your program is busy performing a long operation (e.g. copying a file).
In the event that you are running a local loop which calls this function continuously, without an event loop, the DeferredDelete events will not be processed. This can affect the behaviour of widgets, e.g. QToolTip, that rely on DeferredDelete events to function properly. An alternative would be to call sendPostedEvents() from within that local loop.
Calling this function processes events only for the calling thread, and returns after all available events have been processed. Available events are events queued before the function call. This means that events that are posted while the function runs will be queued until a later round of event processing.

以我这个英语不咋地的水平理解,就是处理消息队列中的所有消息。所谓消息,就好像微软常说的事件,比如点击一个按钮,可以写它的响应事件。qt里是信号和槽。这东西大多是异步的,别人给它发消息,都是先入队列,待会儿它会调用这个函数统一处理。流程为了保持响应,需要轮巡处理消息事件,所以就叫事件循环。上述英文也说了,耗时操作要时不时调用它一下,就像游泳要喘气。

在下面提到的exec()函数中,就与之有关,贴上一段qt源码:

int QEventLoop::exec(ProcessEventsFlags flags)
{
    ...

    while (!d->exit.loadAcquire())
        processEvents(flags | WaitForMoreEvents | EventLoopExec);

    ...
}

可以看到,exec()就是不断调用processEvents实现的。

Qt源码调试步骤记录-CSDN博客

场景——main函数:

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

#include "mainwindow.h"
#include <QApplication>

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

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

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

#include "mainwindow.h"
#include <QApplication>

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当中使用lambda写匿名槽函数。而写匿名槽函数的时候,接收者千万别写this,this指针是指向父线程的线程对象,能作为句柄控制线程,但this隶属于父线程。所以一旦写了this是接收者,这个匿名槽函数会在父线程执行。而子线程中创建对象时,也不要指定parent为this。

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

场景——子线程对象

上面说过线程的实现,离不开父线程的线程对象,它仅仅是子线程的操作句柄。qt源码以及qt手册的“QThread Class>Detailed Description”中的说明很到位。建议初学朋友认真阅读。

/*!
    ...

    A QThread object manages one thread of control within the
    program. QThreads begin executing in run(). By default, run() starts the
    event loop by calling exec() and runs a Qt event loop inside the thread.

    You can use worker objects by moving them to the thread using
    QObject::moveToThread().

    ...

    Another way to make code run in a separate thread, is to subclass QThread
    and reimplement run(). For example:

    ...
*/

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

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

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

如果是不太繁忙的工作,可能不需要考虑下面的问题。但我写了一个模拟生产者的逻辑,使用了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 cpu 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的时候才有机会被外部更改。(这句话您凑合着先这样认为,其实不严谨,因为线程被os调度,什么时候切换到另一个线程不好把握,所以多线程代码的可靠性要禁得起推敲。这里只是举个例子,先这么认为。)

所以上面的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在不恰当的时机发生。

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

建议看其它场景和更严谨的方法:

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

  • 46
    点赞
  • 99
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
### 回答1: b'qcoreapplication::processevents()'是Qt框架中的一个函数,用来处理事件循环中的事件。该函数会按照事件的优先级依次处理事件队列中的所有事件。在GUI程序中,通常会在主线程中调用该函数来处理各类事件,例如响应用户输入、更新界面显示等。 ### 回答2: qCoreApplication::processEvents()是Qt框架中的一个函数,用于处理尚未处理的事件。在Qt应用程序中,事件是Qt框架用来传递消息和处理用户输入的基本机制。 该函数的作用是处理Qt事件队列中的所有待处理事件。它将按照事件的优先级进行处理,优先级高的事件先被处理。处理事件的过程包括触发已注册的事件处理器、执行相关的函数和更新用户界面等操作。 在应用程序执行过程中,当有事件需要处理时,它们会被放入事件队列中,等待被处理。这些事件可以来自用户输入(如鼠标点击、键盘输入等),也可以来自系统(如定时器事件、网络事件等)。 调用qCoreApplication::processEvents()函数是为了尽快处理事件队列中的待处理事件,以保证用户界面的流畅性和响应能力。在长时间的计算或循环执行过程中,如果不及时处理事件,用户无法与应用程序进行交互,会导致应用程序无响应的状态。 需要注意的是,过度频繁地调用qCoreApplication::processEvents()函数可能会导致程序性能下降,所以应该根据实际情况来合理使用该函数。在一些特定场景下,如在主线程中处理耗时操作时,可以使用 QEventLoop::processEvents() 代替 qCoreApplication::processEvents(),以避免出现事件积压的情况。 总之,qCoreApplication::processEvents()是Qt框架中用于处理待处理事件队列的函数,通过调用它可以保证应用程序的响应能力和用户界面的流畅性。 ### 回答3: qCoreapplication::processEvents()是Qt中的一个函数,用于处理当前线程的事件队列,它会依次处理事件队列中的每个事件。 在Qt中,每个事件都被封装为一个QEvent对象,包括用户操作、系统事件、定时器事件等等。当程序运行时,这些事件会依次被添加到当前线程的事件队列中。 当调用qCoreapplication::processEvents()时,它会开始处理事件队列中的事件。它会循环遍历事件队列,依次处理每个事件,直到事件队列为空,或者达到特定的退出条件。 在处理事件时,QCoreapplication::processEvents()会根据事件的类型,调用对应的事件处理函数。比如,对于键盘事件,会调用键盘事件处理函数,对于鼠标事件,则调用鼠标事件处理函数,以此类推。 处理事件的过程中,可能会触发一些信号与槽的连接,也可能会改变程序的状态。比如,当接收到一个按钮点击事件时,如果与该按钮相关联的槽函数被调用,那么可能会触发界面的更新或者其他逻辑的执行。 QCoreapplication::processEvents()的调用通常发生在事件循环之外的地方,比如在长时间的计算过程中插入一些GUI响应,或者在程序启动后手动更新界面。 需要注意的是,过多地频繁调用qCoreapplication::processEvents()可能会导致程序性能下降,因为每次调用都会进行一次事件处理过程。所以,应该谨慎使用,并确保合理的调用时机。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值