最近一直在倒騰事件循環的東西,經過查看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()造成局部事件循環。
整體來講,須要記住如下幾點: