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

本文详细探讨了Qt中的线程和事件循环机制,包括QThread、QCoreApplication::processEvents()、exec()等关键函数的使用。通过多个场景分析,解释了如何在多线程中控制流程、实现线程通信以及线程安全的退出。同时,强调了事件循环在信号槽机制和资源管理中的重要性,并提供了避免常见错误的建议。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

背景:

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

【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);

    ...
}

int QThread::exec()
{
    ...

    QEventLoop eventLoop;
    int returnCode = eventLoop.exec();

    ...
}

 可以看到,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();//事件循环
}

以及一段qt源码:

/*!
    The starting point for the thread. After calling start(), the
    newly created thread calls this function. The default
    implementation simply calls exec().

    You can reimplement this function to facilitate advanced thread
    management. Returning from this method will end the execution of
    the thread.

    \sa start(), wait()
*/
void QThread::run()
{
    (void) exec();
}

 跟main函数一样,这里必须写事件循环,否则run函数执行完前面代码之后会直接结束,线程就结束了。前提是exec之前没有while(1)这样的工作循环。

个人感觉,某种角度可以粗略地认为run函数相当于QThread中的“main函数”。比如嵌入式的main函数中,通常有个while(1)工作循环,就是不断让它执行轮巡作业。而重写run函数时,也可以这么干。这种方式就不用调用exec函数了。本质上道理是一样的。

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

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

 因为有朋友提出质疑,我特意又看了源码,下面贴出源码中的一段注释。注释中,刚好说到了我总结的一些内容。还是人家作者写的到位。真心的,多看源码和注释,有时候比读书管用。强烈建议展开下面的注释仔细阅读,其中我把自己认为重要的部分加了中文注释:

/*!
    \class QThread
    \inmodule QtCore
    \brief The QThread class provides a platform-independent way to
    manage threads.

    \ingroup thread

    //下面这段说明了使用QThread的推荐方式moveToThread,以及run函数和事件循环的关系。

    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().

    \snippet code/src_corelib_thread_qthread.cpp worker

    The code inside the Worker's slot would then execute in a
    separate thread. However, you are free to connect the
    Worker's slots to any signal, from any object, in any thread. It
    is safe to connect signals and slots across different threads,
    thanks to a mechanism called \l{Qt::QueuedConnection}{queued
    connections}.

    //下面说明了另一种,重写run函数的方式。以及exec函数的必要性。

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

    \snippet code/src_corelib_thread_qthread.cpp reimpl-run

    In that example, the thread will exit after the run function has returned.
    There will not be any event loop running in the thread unless you call
    exec().

    //下面这段说明了线程对象问题,其实很简单,但容易被忽略。它隶属于父线程。

    It is important to remember that a QThread instance \l{QObject#Thread
    Affinity}{lives in} the old thread that instantiated it, not in the
    new thread that calls run(). This means that all of QThread's queued
    slots and \l {QMetaObject::invokeMethod()}{invoked methods} will execute
    in the old thread. Thus, a developer who wishes to invoke slots in the
    new thread must use the worker-object approach; new slots should not be
    implemented directly into a subclassed QThread.

    //下面这段说明了函数调用中,可能发生的线程安全问题。说白了,只有run函数是运行于子线程的。
 
    Unlike queued slots or invoked methods, methods called directly on the
    QThread object will execute in the thread that calls the method. When
    subclassing QThread, keep in mind that the constructor executes in the
    old thread while run() executes in the new thread. If a member variable
    is accessed from both functions, then the variable is accessed from two
    different threads. Check that it is safe to do so.

    \note Care must be taken when interacting with objects across different
    threads. See \l{Synchronizing Threads} for details.

    \section1 Managing Threads

    QThread will notifiy you via a signal when the thread is
    started() and finished(), or you can use isFinished() and
    isRunning() to query the state of the thread.

    You can stop the thread by calling exit() or quit(). In extreme
    cases, you may want to forcibly terminate() an executing thread.
    However, doing so is dangerous and discouraged. Please read the
    documentation for terminate() and setTerminationEnabled() for
    detailed information.

    From Qt 4.8 onwards, it is possible to deallocate objects that
    live in a thread that has just ended, by connecting the
    finished() signal to QObject::deleteLater().

    Use wait() to block the calling thread, until the other thread
    has finished execution (or until a specified time has passed).

    QThread also provides static, platform independent sleep
    functions: sleep(), msleep(), and usleep() allow full second,
    millisecond, and microsecond resolution respectively. These
    functions were made public in Qt 5.0.

    \note wait() and the sleep() functions should be unnecessary in
    general, since Qt is an event-driven framework. Instead of
    wait(), consider listening for the finished() signal. Instead of
    the sleep() functions, consider using QTimer.

    The static functions currentThreadId() and currentThread() return
    identifiers for the currently executing thread. The former
    returns a platform specific ID for the thread; the latter returns
    a QThread pointer.

    To choose the name that your thread will be given (as identified
    by the command \c{ps -L} on Linux, for example), you can call
    \l{QObject::setObjectName()}{setObjectName()} before starting the thread.
    If you don't call \l{QObject::setObjectName()}{setObjectName()},
    the name given to your thread will be the class name of the runtime
    type of your thread object (for example, \c "RenderThread" in the case of the
    \l{Mandelbrot Example}, as that is the name of the QThread subclass).
    Note that this is currently not available with release builds on Windows.

    \sa {Thread Support in Qt}, QThreadStorage, {Synchronizing Threads},
        {Mandelbrot Example}, {Semaphores Example}, {Wait Conditions Example}
*/

为了证实我的观点,看过这段注释之后更加坚信是没错的。其中一段已经非常明确:

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

    \snippet code/src_corelib_thread_qthread.cpp reimpl-run

    In that example, the thread will exit after the run function has returned.
    There will not be any event loop running in the thread unless you call
    exec().

可以看到,在重写run函数的方式下,exec函数的作用。

场景——子线程对象

上面说过线程的实现,离不开父线程的线程对象,它仅仅是子线程的操作句柄。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博客

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值