最近一直在倒腾事件循环的东西,经过查看Qt源码多少仍是有点心得体会,在这里记录下和你们分享。总之,对于QStateMachine状态机自己来讲,须要有QEventLoop::exec()的驱动才能支持,也就是说,在你Qt程序打开的时候,最后一句html
QCoreApplication::exec()
已经由内部进入了状态循环app
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,咱们知道QDialog有相似的exec()函数,其实内部也会进入一个局部的事件循环:async
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来进行事件循环。这个时候,确定有同窗会有以下疑问:函数
其实答案在上面已经有了,对于一个线程来讲,其所拥有的事件队列是惟一的,但其所拥有的事件循环能够是多个,但绝对是嵌套关系,而且是只有当前QEventLoop被激活。咱们能够看QEventLoop的exec()内部究竟在作什么。oop
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():post
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); }
能够很明显的看到,对于一个线程来讲,不管其事件循环是内层嵌套仍是在外层,其最终都会去调用ui
d->threadData->eventDispatcher
这个是线程惟一的,从而也证实了咱们上面的结论,事件队列对于线程来讲是一对一的。那么如何来验证咱们另外一个观点,即在同一个线程上事件循环能够是多个,而且是嵌套关系,当前只有一个激活呢?咱们写一个小的Demo来验证一下:this
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以前和打开以后调用栈的区别:spa
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()造成局部事件循环。
整体来讲,须要记住如下几点: