QT核心-事件循环

事件循环

接着上一章没说完的继续,事件循环的源码其实不太好刨,它是和平台相关的,比如Windows上它用了PostMessage、PeekMessage和WaitForSingleObject等一系列Windows API完成的,当然线程也是调用平台API,所以这里不过多关注其源码,而是更多的理解我们能用到的功能。
上一章总结其实就一句话: 生存线程就是QObject的event方法的执行线程。通过方法名我们就能知道这个方法是和事件相关的,它并不会被线程直接调用,而是由QEventLoop内部去调用,我们看一下QThread的run方法的默认实现和与之相关的exec方法:

void QThread::run()
{
    (void) exec();
}
int QThread::exec()
{
    Q_D(QThread);
    QMutexLocker locker(&d->mutex);
    d->data->quitNow = false;
    if (d->exited) {
        d->exited = false;
        return d->returnCode;
    }
    locker.unlock();

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

    locker.relock();
    d->exited = false;
    d->returnCode = -1;
    return returnCode;
}

所以说如果直接只用QThread::run的默认实现或者重写run方法之后手动调用过exec的话整个线程将由QEventLoop接管。

QEventLoop
  • 先说一下QEventLoop的用法,正如上面QThread::exec中的代码,开启一个新的事件循环只需要两行代码:
QEventLoop eventLoop;
int returnCode = eventLoop.exec();

当程序执行到int returnCode = eventLoop.exec();时,会卡在exec函数里,这里面其实是个死循环,它会提取当前线程的事件队列中的事件并分配给相应对象执行(由QCoreApplication::postEvent发送),所以它不会影响其他事件继续执行,仅仅只是当前流程会停下而已,退出该事件循环为调用eventLoop.quit(),注意看下面例子。那么它和while(true){}这种形式的区别就很明显了,while(true)是直接将当前线程卡死在这,什么都不执行。下面举个例子区分这两个:

  1. while(true)方式:
    EventLoopTest.h
#pragma once

#include <QObject>

class EventLoopTest : public QObject
{
    Q_OBJECT
public:
    explicit EventLoopTest(QObject *parent = nullptr);

    void test();

protected:
    bool event(QEvent *event) override;
};

EventLoopTest.cpp

#include "EventLoopTest.h"

#include <QCoreApplication>
#include <QDateTime>
#include <QDebug>
#include <QEvent>
#include <QEventLoop>
#include <QTimer>

EventLoopTest::EventLoopTest(QObject *parent) : QObject(parent)
{}

void EventLoopTest::test()
{
    qDebug() << "into test:" << QDateTime::currentDateTime().toString(Qt::ISODateWithMs);
    QCoreApplication::postEvent(this, new QEvent(static_cast<QEvent::Type>(QEvent::User)));
    auto start = QDateTime::currentMSecsSinceEpoch();
    while (QDateTime::currentMSecsSinceEpoch() - start < 1000) {
    }
    qDebug() << "out test:" << QDateTime::currentDateTime().toString(Qt::ISODateWithMs);
}

bool EventLoopTest::event(QEvent *event)
{
    if (event->type() == QEvent::User) {
        qDebug() << "into event:" << QDateTime::currentDateTime().toString(Qt::ISODateWithMs);
    }
    return QObject::event(event);
}

main.cpp

#include "MainWindow.h"

#include <QApplication>
#include <QTimer>

#include "EventLoopTest.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    //! 延迟100毫秒再构造EventLoopTest并执行其test方法
    //! 主要目的是等待a.exec()方法执行,它会开启一个最根本的事件循环
    //! 当然不延迟直接构造执行也可以
    //! 这里要注意,这个lambda表达式是a.exec()内部的事件循环调用的(暂时不理解也不影响)
    QTimer::singleShot(100, [&w]() {
        auto e = new EventLoopTest(&w);
        e->test();
    });
    return a.exec();
}

输出结果

into test: "2021-03-14T15:39:52.738"
out test: "2021-03-14T15:39:53.739"
into event: "2021-03-14T15:39:53.739"

根据上面的输出可以明显看到,while(true)的暂停程序流程方式一定是等到test结束之后再执行的event方法。该event方式是由main函数中的a.exec();里面的事件循环调用的。
2. QEventLoop方式:
其他代码完全不变,将test方法中的代码换成下面

qDebug() << "into test:" << QDateTime::currentDateTime().toString(Qt::ISODateWithMs);
QCoreApplication::postEvent(this, new QEvent(static_cast<QEvent::Type>(QEvent::User)));
QEventLoop l;
//! 等待1秒后调用对象l中的quit方法
QTimer::singleShot(1000, &l, &QEventLoop::quit);
l.exec();
qDebug() << "out test:" << QDateTime::currentDateTime().toString(Qt::ISODateWithMs);

输出结果

into test: "2021-03-14T15:54:11.513"
into event: "2021-03-14T15:54:11.514"
out test: "2021-03-14T15:54:12.515"

由上面结果可以看到,即使test方法还没有退出,event方法也能被正确执行,这就是QEventLoop的作用。

  • 下面举一个平常开始时经常用到的例子
    注意: 使用这个例子时可能会出现由https引发的openssl库的问题,请先自行百度解决一下。
    NetworkTest.h
#pragma once

#include <QObject>

class NetworkTest : public QObject
{
    Q_OBJECT
public:
    explicit NetworkTest(QObject *parent = nullptr);

    void req();
};

NetworkTest.cpp

#include "NetworkTest.h"

#include <QEventLoop>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QTimer>

NetworkTest::NetworkTest(QObject *parent) : QObject(parent)
{}

void NetworkTest::req()
{
    QNetworkAccessManager man;
    QNetworkRequest req;
    req.setUrl(QUrl("https://www.baidu.com/"));
    auto reply = man.get(req);
    QEventLoop l;
    QTimer::singleShot(1000, &l, &QEventLoop::quit);
    l.exec();
    qDebug() << "read:" << reply->readAll();
    reply->deleteLater();
}

main.cpp

#include "MainWindow.h"

#include <QApplication>
#include <QTimer>

#include "NetworkTest.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    //! 延迟100毫秒再构造NetworkTest并执行其req方法
    //! 主要目的是避免迷惑,等待a.exec()方法执行并由其内部开启一个默认事件循环
    //! 这里直接构造并执行也没问题,这里这样写主要是给一些爱深想的人提供一个例子,不用太过纠结
    QTimer::singleShot(100, [&w]() {
        auto n = new NetworkTest(&w);
        n->req();
    });
    return a.exec();
}

输出结果

read: "<html>\r\n<head>\r\n\t<script>\r\n\t\tlocation.replace(location.href.replace(\"https://\",\"http://\"));\r\n\t</script>\r\n</head>\r\n<body>\r\n\t<noscript><meta http-equiv=\"refresh\" content=\"0;url=http://www.baidu.com/\"></noscript>\r\n</body>\r\n</html>"

当然,这里的数据可能不全,但无关紧要,这里只是演示,我们主要是想以同步的方式接收网络请求的数据。正常用法是将QTimer::singleShot(1000, &l, &QEventLoop::quit);这行代码换成connect(reply, &QNetworkReply::finished, &l, &QEventLoop::quit);
上面req方法里面换成while(true)形式是永远没办法读取到数据的

QNetworkAccessManager man;
QNetworkRequest req;
req.setUrl(QUrl("https://www.baidu.com/"));
auto reply = man.get(req);
auto start = QDateTime::currentMSecsSinceEpoch();
while (QDateTime::currentMSecsSinceEpoch() - start < 1000) {
}
qDebug() << "read:" << reply->readAll();
reply->deleteLater();

输出结果永远是read: ""
这里可以大胆猜测,QNetworkReply内部从socket读取数据是依赖事件循环的,如上面使用QEventLoop开启一个子事件循环,QNetworkReply中的数据读取方法仍能正常执行。而使用while(true)的方式时,当前线程永远被卡在这个死循环里,QNetworkReply无法正常读取数据。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值