深刻理解QStateMachine與QEventLoop事件循環的聯繫與區別

最近一直在倒騰事件循環的東西,經過查看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();
...
}

由上面咱們能夠獲得如下幾個結論:異步

  1. 很天然而然的咱們能夠看到,事件隊列只跟線程有關,即同一個線程,如論你如何更改,最終你的事件循環和事件隊列自己都是屬於這個線程的。
  2. QApplication::exec()這種都會去最終調用QEventLoop::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來進行事件循環。這個時候,確定有同窗會有以下疑問:函數

  • 那若是我在QApplication::exec()中調用了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()造成局部事件循環。

整體來講,須要記住如下幾點:

  • 事件隊列對於線程來講是一對一的,而事件循環對於線程來講是多對一的,但他們是嵌套關係,而且只有當前QEventLoop被激活。
  • 狀態機的驅動須要經過現存的事件循環來推進,而且其內部維護的事件隊列和QEventLoop的事件隊列是兩回事。
  • 當狀態機的_q_process()沒有返回的時候,Qt不會再去派發_q_process事件。而且總會在_q_process循環中針對當前的全部狀態機事件進行逐步處理。
相關文章
相關標籤/搜索