深入理解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();
...
}

由上面我們可以得到以下幾個結論:

  1. 很自然而然的我們可以看到,事件隊列只跟線程有關,即同一個線程,如論你如何更改,最終你的事件循環和事件隊列本身都是屬於這個線程的。
  2. 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循環中針對當前的所有狀態機事件進行逐步處理。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM