深入理解QStateMachine与QEventLoop事件循环的联系与区别
最近一直在倒腾事件循环的东西,通过查看Qt源码多少还是有点心得体会,在这里记录下和大家分享。总之,对于QStateMachine状态机本身来说,需要有QEventLoop::exec()的驱动才能支持,也就是说,在你Qt程序打开的时候,最后一句
QCoreApplication::exec()
已经由内部进入了状态循环
int QCoreApplication::exec()
{
...
QThreadData *threadData = self->d_func()->threadData;
if (threadData != QThreadData::current()) {
qWarning("%s::exec: Must be called from the main thread", self->metaObject()->className());
return -;
}
if (!threadData->eventLoops.isEmpty()) {
qWarning("QCoreApplication::exec: The event loop is already running");
return -;
} QEventLoop eventLoop;
self->d_func()->in_exec = true;
self->d_func()->aboutToQuitEmitted = false;
int returnCode = eventLoop.exec();
...
}
由上面我们可以得到以下几个结论:
- 很自然而然的我们可以看到,事件队列只跟线程有关,即同一个线程,如论你如何更改,最终你的事件循环和事件队列本身都是属于这个线程的。
- QApplication::exec()这种都会去最终调用QEventLoop::exec()形成事件循环。
其实不仅仅是QApplication,我们知道QDialog有类似的exec()函数,其实内部也会进入一个局部的事件循环:
int QDialog::exec()
{
...
QEventLoop eventLoop;
d->eventLoop = &eventLoop;
QPointer<QDialog> guard = this;
(void) eventLoop.exec(QEventLoop::DialogExec);
if (guard.isNull())
return QDialog::Rejected;
d->eventLoop = ;
...
}
可以看到,QDialog的这种exec()其实内部也是最终产生了一个栈上的QEventLoop来进行事件循环。这个时候,肯定有同学会有如下疑问:
- 那如果我在QApplication::exec()中调用了QDialog的exec(),那QEventLoop如何来分配指责?
其实答案在上面已经有了,对于一个线程来说,其所拥有的事件队列是唯一的,但其所拥有的事件循环可以是多个,但绝对是嵌套关系,并且是只有当前QEventLoop被激活。我们可以看QEventLoop的exec()内部究竟在做什么。
int QEventLoop::exec(ProcessEventsFlags flags)
{
Q_D(QEventLoop);
...#if defined(QT_NO_EXCEPTIONS)
while (!d->exit)
processEvents(flags | WaitForMoreEvents | EventLoopExec);
#else
try {
while (!d->exit)
processEvents(flags | WaitForMoreEvents | EventLoopExec);
} catch (...) {
...
}
可以看到其内部正是在通过一个while循环去不断的processEvents(),我们再来看processEvents():
bool QEventLoop::processEvents(ProcessEventsFlags flags)
{
Q_D(QEventLoop);
if (!d->threadData->eventDispatcher)
return false;
if (flags & DeferredDeletion)
QCoreApplication::sendPostedEvents(, QEvent::DeferredDelete);
return d->threadData->eventDispatcher->processEvents(flags);
}
可以很明显的看到,对于一个线程来说,无论其事件循环是内层嵌套还是在外层,其最终都会去调用
d->threadData->eventDispatcher
这个是线程唯一的,从而也证明了我们上面的结论,事件队列对于线程来说是一对一的。那么如何来验证我们另一个观点,即在同一个线程上事件循环可以是多个,并且是嵌套关系,当前只有一个激活呢?我们写一个小的Demo来验证一下:
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
} MainWindow::~MainWindow()
{
delete ui;
} void MainWindow::on_pushButton_clicked()
{
QDialog dialog;
dialog.exec();
}
很简单,我们在MainWindow上放一个button,他的点击函数会出现一个dialog并且进入局部事件循环,之后我们在QEventLoop::exec()下断点,分别查看在没打开Dialog之前和打开之后调用栈的区别:
QEventLoop::processEvents qeventloop.cpp 0xb717dfc3
QEventLoop::exec qeventloop.cpp 0xb717e1cf
QCoreApplication::exec qcoreapplication.cpp 0xb7181098
QApplication::exec qapplication.cpp 0xb74c7eaa
main main.cpp 0x804a4ce
这是没打开Dialog之前,可以看到此时的事件循环正是QCoreApplication内部提供的QEventLoop。当我们打开Dialog之后再来查看
QEventLoop::processEvents qeventloop.cpp 0xb717dfc3
QEventLoop::exec qeventloop.cpp 0xb717e1cf
QDialog::exec qdialog.cpp 0xb7a949c4
...
QEventDispatcherGlib::processEvents qeventdispatcher_glib.cpp 0xb71b7cc6
QGuiEventDispatcherGlib::processEvents qguieventdispatcher_glib.cpp 0xb7595140
QEventLoop::processEvents qeventloop.cpp 0xb717e061
QEventLoop::exec qeventloop.cpp 0xb717e1cf
QCoreApplication::exec qcoreapplication.cpp 0xb7181098
QApplication::exec qapplication.cpp 0xb74c7eaa
main main.cpp 0x804a4ce
可以看到此时的事件循环正是QDialog的exec(),其实也很好理解,内部的exec()不退出,自然就不能运行外部的exec(),但千万别以为此时就事件阻塞了,很多人跟我一样,一开始总以为QDialog::exec()就会造成事件阻塞,其实事件循环依旧在不断处理,唯一的区别就是这时的事件循环是在QDialog上。
理解了基本的事件循环和事件队列之后,让我们再来看一下QStateMachine与事件循环的关联:
首先我们来看一下QStateMachine自己的postEvent()
void QStateMachine::postEvent(QEvent *event, EventPriority priority)
{
...
switch (priority) {
case NormalPriority:
d->postExternalEvent(event);
break;
case HighPriority:
d->postInternalEvent(event);
break;
}
d->processEvents(QStateMachinePrivate::QueuedProcessing);
}
可以看到,他其实内部自己维护了两个队列,一个是普通优先级的externalEventQueue,一个是高优先级的internalEventQueue。由此我们也可以得出Qt官方文档所说的状态机的事件循环和队列跟我们上文提的事件队列和事件循环压根就是两码事,千万别搞混了。可以看到他内部也会进行processEvents(),我们来看一下:
void QStateMachinePrivate::processEvents(EventProcessingMode processingMode)
{
Q_Q(QStateMachine);
if ((state != Running) || processing || processingScheduled)
return;
switch (processingMode) {
case DirectProcessing:
if (QThread::currentThread() == q->thread()) {
_q_process();
break;
} // fallthrough -- processing must be done in the machine thread
case QueuedProcessing:
processingScheduled = true;
QMetaObject::invokeMethod(q, "_q_process", Qt::QueuedConnection);
break;
}
}
很显然,状态机的实现逻辑就是把_q_process()这个异步调用,放到事件队列中去,这也印证了官方文档所说的
Note that this means that it executes asynchronously, and that it will not progress without a running event loop.
这句话,也就是说状态机的运转就是向当前线程的事件队列丢一个_q_process(),然后等待事件循环给他进行调用,所以接下来问题的关键就是_qt_process()
void QStateMachinePrivate::_q_process()
{
...
Q_Q(QStateMachine);
Q_ASSERT(state == Running);
Q_ASSERT(!processing);
processing = true;
processingScheduled = false; while (processing) {
if (stop) {
processing = false;
break;
}
QSet<QAbstractTransition*> enabledTransitions;
QEvent *e = new QEvent(QEvent::None);
enabledTransitions = selectTransitions(e);
if (enabledTransitions.isEmpty()) {
delete e;
e = ;
}
...
enabledTransitions = selectTransitions(e);
if (enabledTransitions.isEmpty()) {
delete e;
e = ;
}
}
if (!enabledTransitions.isEmpty()) {
q->beginMicrostep(e);
microstep(e, enabledTransitions.toList());
q->endMicrostep(e);
}#endif
if (stop) {
stop = false;
stopProcessingReason = Stopped;
...
}
可以看到,状态机的process本身就是一个大循环,flag为processing(这也是避免多次投递_q_process()的标记位),进入此函数后状态机会去根据状态迁移表去调用相应的函数。这里面其实也有可以扩展的地方,就是当我的状态机本身去调用的函数是一个不返回的,也就是说比如QDialog::exec(),进入了事件循环,那我此时的状态机会卡在
microstep(e, enabledTransitions.toList());
这个函数上,我们也知道exec()函数可以让我们正常进行事件派发,所以当事件队列又去调用状态机事件的时候,因为上文processing这个flag的存在,我们在
void QStateMachinePrivate::processEvents(EventProcessingMode processingMode)
{
Q_Q(QStateMachine);
if ((state != Running) || processing || processingScheduled)
return;
...
}
会立即返回,所以你也不需要去担心状态机的阻塞以及效率问题,因为此时他只做队列的post维护,但processEvents()压根不能执行。
这个问题还有一个有意思的地方是需要注意的,就拿我们之前的语境,状态机本身调用的函数会去调用一个QDialog::exec(),那么在创建好dialog之后,我的事件循环就在这个dialog中的QEventLoop开始做了,所以有一点需要注意就是我的_q_process()
void QStateMachinePrivate::_q_process()
{
Q_Q(QStateMachine);
Q_ASSERT(state == Running);
Q_ASSERT(!processing);
processing = true;
processingScheduled = false;
#ifdef QSTATEMACHINE_DEBUG
qDebug() << q << ": starting the event processing loop";
#endif
while (processing) {
if (stop) {
processing = false;
break;
}
...
}
因为while循环的存在,所以我的队列可能此时有3个事件,A,B,C,其中我执行A的时候我创建了个Dialog,此时我的所有事件循环都建立在这个新创建的dialog的内部的那个QEventLoop,那么当我关闭这个Dialog的时候,我while继续执行,但此时我所在的事件循环已经是QCoreApplication的exec内部的QEventLoop了,这点需要特别注意。
还有一个需要注意的是倘若你想让状态机在执行耗时函数的时候可以立即返回或者像上文一样出现Dialog,此时状态机不能继续循环,但你的需要是想让状态机可以继续正常运行处理别的事件的时候,你就需要在状态机处理事件的内部调用
bool QMetaObject::invokeMethod();
这个函数,通过第三个参数选择Qt::QueuedConnection你可以很轻松的把这个dialog投递当QEventLoop的事件队列中,而让当前状态机正常返回,然后QEventLoop的processEvents()会去处理这个dialog,并创建之后调用exec()形成局部事件循环。
总体来说,需要记住以下几点:
- 事件队列对于线程来说是一对一的,而事件循环对于线程来说是多对一的,但他们是嵌套关系,并且只有当前QEventLoop被激活。
- 状态机的驱动需要通过现存的事件循环来推动,并且其内部维护的事件队列和QEventLoop的事件队列是两回事。
- 当状态机的_q_process()没有返回的时候,Qt不会再去派发_q_process事件。并且总会在_q_process循环中针对当前的所有状态机事件进行逐步处理。
深入理解QStateMachine与QEventLoop事件循环的联系与区别的更多相关文章
- 深入理解javascript中的事件循环event-loop
前面的话 本文将详细介绍javascript中的事件循环event-loop 线程 javascript是单线程的语言,也就是说,同一个时间只能做一件事.而这个单线程的特性,与它的用途有关,作为浏览器 ...
- 理解 node.js 的事件循环
node.js 的第一个基本观点是,I/O 操作是昂贵的: 目前的编程技术最大的浪费来自等待 I/O 操作的完成.有几种方法可以解决这些对性能的影响(来自Sam Rushing): 同步:依次处理单个 ...
- Qt事件循环与状态机事件循环的思考
写下这个给自己备忘,关于事件循环以及多线程方面的东西我还需要多多学习.首先我们都知道程序有一个主线程,在GUI程序中这个主线程也叫GUI线程,图形和绘图相关的函数都是由主线程来提供.主线程有个事件循环 ...
- JS:事件循环机制、调用栈以及任务队列
点击查看原文 写在前面 js里的事件循环机制十分有趣.从很多面试题也可以看出来,考察简单的setTimeout也就是考察这个机制的. 在之前,我只是简单地认为由于函数执行很快,setTimeout执行 ...
- QT中的线程与事件循环理解(1)
1.需要使用多线程管理的例子 一个进程可以有一个或更多线程同时运行.线程可以看做是“轻量级进程”,进程完全由操作系统管理,线程即可以由操作系统管理,也可以由应用程序管理.Qt 使用QThread 来管 ...
- QT虚拟小键盘设计--qt事件循环,事件发送的理解
有人讲到QT5.7及其以后的版本才自带免费的小键盘插件. QT5.10中关于QKeyEvent类:点击打开链接 QT sendEvent和PostEvent, 点击打开链接 my god,我今天安装了 ...
- JavaScript:彻底理解同步、异步和事件循环(Event Loop) (转)
原文出处:https://segmentfault.com/a/1190000004322358 一. 单线程 我们常说"JavaScript是单线程的". 所谓单线程,是指在JS ...
- 关于Qt的事件循环以及QEventLoop的简单使用
1.一般我们的事件循环都是由exec()来开启的,例如下面的例子: 1 QCoreApplicaton::exec() 2 QApplication::exec() 3 QDialog::exec() ...
- 深入理解 JavaScript 事件循环(一)— event loop
引言 相信所有学过 JavaScript 都知道它是一门单线程的语言,这也就意味着 JS 无法进行多线程编程,但是 JS 当中却有着无处不在的异步概念 .在初期许多人会把异步理解成类似多线程的编程模式 ...
随机推荐
- String类、 StringBuffer、基本数据类型对象包装类
一.概述 Sting s1 = "abc"; //s1是一个类类型变量,"abc"是一个对象. String s2 = new String(" ...
- Jmeter聚合报告分析
Label:每个 JMeter 的 element(例如 HTTP Request)都有一个 Name 属性,这里显示的就是 Name 属性的值 Average:平均响应时间--默认情况下是单个 Re ...
- SDK更新太慢
同时,更新ADT和SDK Manager 在SDK Manager下Tools->Options打开了SDK Manager的Settings,选中“Force https://… source ...
- POJ 3734
题目的大意: 给定待粉刷的n个墙砖(排成一行),每一个墙砖能够粉刷的颜色种类为:红.蓝.绿.黄, 问粉刷完成后,红色墙砖和蓝色墙砖都是偶数的粉刷方式有多少种(结果对10007取余). 解题思路: 思路 ...
- [Redux] Wrapping dispatch() to Log Actions
We will learn how centralized updates in Redux let us log every state change to the console along wi ...
- careercup-数组和字符串1.2
1.2 用C或C++实现void reverse(char *str)函数,即反转一个null结尾的字符串. C++实现代码: #include<iostream> #include< ...
- Windows与Linux下文件操作监控的实现
一.需求分析: 随着渲染业务的不断进行,数据传输渐渐成为影响业务时间最大的因素.究其原因就是因为数据传输耗费较长的时间.于是,依托于渲染业务的网盘开发逐渐成为迫切需要解决的需求.该网盘的实现和当前市场 ...
- 装有Win7系统的电脑在局域网不能共享的解决方案
Win7系统的网络功能比XP有了进一步的增强,使用起来也相对清晰.但是由于做了很多表面优化的工作,使得底层的网络设置对于习惯了XP系统的人来说变得很不适应,其中局域网组建就是一个很大的问题.默认安装系 ...
- android自定义控件之滚动广告条
在一些电子商务网站上经常能够看到一些滚动的广告条,许多软件在首次使用时也有类似的广告条,如图: 其实在github上有实现这种效果的控件,不过这东西做起来也是很简单,我们今天就来看看该怎么做. 先来看 ...
- CentOS7添加第三方源
CentOS由于很追求稳定性,所以官方源中自带的软件不多,因而需要一些第三方源,比如EPEL.ATrpms.ELRepo.Nux Dextop.RepoForge等. EPEL EPEL即Extra ...