深入理解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 -1; } if (!threadData->eventLoops.isEmpty()) { qWarning("QCoreApplication::exec: The event loop is already running"); return -1; } 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 = 0; ... }
可以看到,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(0, 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之前和打开之后调用栈的区别:
0 QEventLoop::processEvents qeventloop.cpp 144 0xb717dfc3 1 QEventLoop::exec qeventloop.cpp 204 0xb717e1cf 2 QCoreApplication::exec qcoreapplication.cpp 1225 0xb7181098 3 QApplication::exec qapplication.cpp 3823 0xb74c7eaa 4 main main.cpp 10 0x804a4ce
这是没打开Dialog之前,可以看到此时的事件循环正是QCoreApplication内部提供的QEventLoop。当我们打开Dialog之后再来查看
0 QEventLoop::processEvents qeventloop.cpp 144 0xb717dfc3 1 QEventLoop::exec qeventloop.cpp 204 0xb717e1cf 2 QDialog::exec qdialog.cpp 562 0xb7a949c4 ... 27 QEventDispatcherGlib::processEvents qeventdispatcher_glib.cpp 425 0xb71b7cc6 28 QGuiEventDispatcherGlib::processEvents qguieventdispatcher_glib.cpp 204 0xb7595140 29 QEventLoop::processEvents qeventloop.cpp 149 0xb717e061 30 QEventLoop::exec qeventloop.cpp 204 0xb717e1cf 31 QCoreApplication::exec qcoreapplication.cpp 1225 0xb7181098 32 QApplication::exec qapplication.cpp 3823 0xb74c7eaa 33 main main.cpp 10 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 = 0; } ... enabledTransitions = selectTransitions(e); if (enabledTransitions.isEmpty()) { delete e; e = 0; } } 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循环中针对当前的所有状态机事件进行逐步处理。