事件循环
接着上一章没说完的继续,事件循环的源码其实不太好刨,它是和平台相关的,比如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)
是直接将当前线程卡死在这,什么都不执行。下面举个例子区分这两个:
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无法正常读取数据。