天天看點

Qt中這三個exec,還傻傻分不清嗎?

作者:嵌入式小生

一、導讀

在Qt中,常見到三個exec,第一個是QApplication::exec(),第二個是QEventLoop::exec,第三個是QThread::exec()。本文從源碼角度來看看這三個exec()。

  • QApplication::exec()是QApplication類下的一個靜态成員函數,該函數用于進入主事件循環。
  • QEventLoop::exec是QEventLoop類下的一個公共成員函數,用于進入主事件循環。
  • QThread::exec()是QThread類下的一個受保護的成員函數,也是用于進入事件循環。

都是進入事件循環,他們之間有什麼聯系呢,接着後面的分析。

二、QApplication::exec()

在實際開發中,必須調用QApplication::exec()來啟動事件處理,主事件循環會從視窗系統接收事件,并将這些事件分派給應用程式小部件。在調用exec()之前不能發生任何使用者互動,但是存在一種特殊的情況:QMessageBox這樣的模态小部件可以在調用exec()之前使用,因為模态小部件會調用exec()來啟動本地事件循環。

從源碼角度,QApplication::exec()會調用QGuiApplication::exec(),QGuiApplication::exec()會調用QCoreApplication::exec():

int QCoreApplication::exec()
{
    //檢查exec執行個體
    if (!QCoreApplicationPrivate::checkInstance("exec"))
        return -1;
    
    //擷取線程資料QThreadData
    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;
    }
    
    threadData->quitNow = false;
    //建立QEventLoop事件循環對象
    QEventLoop eventLoop;
    self->d_func()->in_exec = true;
    self->d_func()->aboutToQuitEmitted = false;
    //啟動事件循環
    int returnCode = eventLoop.exec();
    threadData->quitNow = false;

    if (self)
        self->d_func()->execCleanup();

    return returnCode;
}           

從上述源碼可見,QApplication的exec()經過層層調用,最終是使用QEventLoop實作事件循環。

QApplication::exec()用于啟動應用程式的事件循環,讓應用程式能得以啟動運作并接收事件。

『備注,執行應用清理的優雅方式』:

建議将清理代碼連接配接到aboutToQuit()信号,而不是放在應用程式的main()函數中。這是因為,在某些平台上,QApplication::exec()調用可能不會傳回。例如,在Windows平台上,當使用者登出時,系統會在Qt關閉所有頂級視窗後終止該程序。是以,不能保證應用程式有足夠的時間退出事件循環,并在QApplication::exec()調用之後,即在main()函數的末尾執行代碼。

在QCoreApplication::exec()函數實作中的這幾行代碼:

if (threadData != QThreadData::current()) {
        qWarning("%s::exec: Must be called from the main thread", self->metaObject()->className());
        return -1;
    }           

引發出另一個有趣的知識點,那就是:在Qt多線程開發中,需要注意不要阻塞GUI線程,那麼哪個是GUI線程呢?從上述源碼可以明确知道:QApplication a(argc, argv);所線上程就是GUI線程。

三、QThread::exec()

在多線程應用設計中,QThread::exec()用于為目前線程啟動一個新的事件循環,為存在于該線程中的對象傳遞事件。在源碼中,QThread::exec()實作如下:

int QThread::exec()
{
    Q_D(QThread);
    QMutexLocker locker(&d->mutex);
    d->data->quitNow = false;
    if (d->exited) {
        d->exited = false;
        return d->returnCode;
    }
    locker.unlock();
    //建立QEventLoop事件循環。
    QEventLoop eventLoop;
    int returnCode = eventLoop.exec();

    locker.relock();
    d->exited = false;
    d->returnCode = -1;
    return returnCode;
}
           

從源碼角度,也可見QThread::exec()實作中也調用到QEventLoop的exec()。

四、QEventLoop::exec()

QEventLoop::exec()用于進入主事件循環并等待直到exit()被調用。在調用該函數時需要傳入一個flags,如果指定了标志,則隻處理标志允許的類型的事件,Qt中支援以下幾種标志:

序号 标志類型 描述
1 QEventLoop::AllEvents 所有事件
2 QEventLoop::ExcludeUserInputEvents 不處理使用者輸入事件,例如ButtonPress和KeyPress。
3 QEventLoop::ExcludeSocketNotifiers 不要處理套接字通知事件。
4 QEventLoop::WaitForMoreEvents 如果沒有可用的挂起事件,則等待事件。
注,沒有被傳遞的事件不會被丢棄,這些事件将在下次傳入不過濾事件的标志調用procesvents()時被傳遞。

從源碼角度,QEventLoop::exec()實作如下:

int QEventLoop::exec(ProcessEventsFlags flags)
{
    Q_D(QEventLoop);
    auto threadData = d->threadData.loadRelaxed();

    //we need to protect from race condition with QThread::exit
    QMutexLocker locker(&static_cast<QThreadPrivate *>(QObjectPrivate::get(threadData->thread.loadAcquire()))->mutex);
    if (threadData->quitNow)
        return -1;

    if (d->inExec) {
        qWarning("QEventLoop::exec: instance %p has already called exec()", this);
        return -1;
    }

    struct LoopReference {
        QEventLoopPrivate *d;
        QMutexLocker &locker;

        bool exceptionCaught;
        LoopReference(QEventLoopPrivate *d, QMutexLocker &locker) : d(d), locker(locker), exceptionCaught(true)
        {
            d->inExec = true;
            d->exit.storeRelease(false);

            auto threadData = d->threadData.loadRelaxed();
            ++threadData->loopLevel;
            threadData->eventLoops.push(d->q_func());

            locker.unlock();
        }

        ~LoopReference()
        {
            if (exceptionCaught) {
                qWarning("Qt has caught an exception thrown from an event handler. Throwing\n"
                         "exceptions from an event handler is not supported in Qt.\n"
                         "You must not let any exception whatsoever propagate through Qt code.\n"
                         "If that is not possible, in Qt 5 you must at least reimplement\n"
                         "QCoreApplication::notify() and catch all exceptions there.\n");
            }
            locker.relock();
            auto threadData = d->threadData.loadRelaxed();
            QEventLoop *eventLoop = threadData->eventLoops.pop();
            Q_ASSERT_X(eventLoop == d->q_func(), "QEventLoop::exec()", "internal error");
            Q_UNUSED(eventLoop); // --release warning
            d->inExec = false;
            --threadData->loopLevel;
        }
    };
    LoopReference ref(d, locker);

    // 當進入一個新的事件循環時,删除已釋出的exit事件
    QCoreApplication *app = QCoreApplication::instance();
    if (app && app->thread() == thread())
        QCoreApplication::removePostedEvents(app, QEvent::Quit);

#ifdef Q_OS_WASM
    // Partial support for nested event loops: Make the runtime throw a JavaSrcript
    // exception, which returns control to the browser while preserving the C++ stack.
    // Event processing then continues as normal. The sleep call below never returns.
    // QTBUG-70185
    if (threadData->loopLevel > 1)
        emscripten_sleep(1);
#endif

    while (!d->exit.loadAcquire())
        processEvents(flags | WaitForMoreEvents | EventLoopExec);

    ref.exceptionCaught = false;
    return d->returnCode.loadRelaxed();
}           

從上述源碼可知,QEventLoop::exec()本質會調用processEvents()分發事件。processEvents()實作如下:

Qt中這三個exec,還傻傻分不清嗎?

從上圖所示代碼中,會調用QAbstractEventDispatcher::processEvents()實作事件分發。QAbstractEventDispatcher類提供了一個管理Qt事件隊列的接口,它從視窗系統和其他地方接收事件。然後它将這些事件發送到QCoreApplication或QApplication執行個體進行處理和傳遞。

繼續閱讀