Qt 事件系統淺析 (用 Windows API 描述,分析了QCoreApplication::exec()和QEventLoop::exec的源碼)(比起新號槽,事件機制是更高級的抽象,擁有更多特性,比如 accept/ignore,filter,還是實現狀態機等高級 API 的基礎)


事件系統在 Qt 中扮演了十分重要的角色,不僅 GUI 的方方面面需要使用到事件系統,Signals/Slots 技術也離不開事件系統(多線程間)。我們本文中暫且不描述 GUI 中的一些特殊情況,來說說一個非 GUI 應用程序的事件模型。

如果讓你寫一個程序,打開一個套接字,接收一段字節然后輸出,你會怎么做?

int main(int argc, char *argv[]) { WORD wVersionRequested; WSADATA wsaData; SOCKET sock; int err; BOOL bSuccess; wVersionRequested = MAKEWORD(2, 2); err = WSAStartup(wVersionRequested, &wsaData); if (err != 0) return 1; sock = WSASocketW(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0); if (sock == INVALID_SOCKET) return 1; bSuccess = WSAConnectByName(sock, const_cast<LPWSTR>(L"127.0.0.1"), ...); if (!bSuccess) return 1; WSARecv(sock, &wsaData, ...); WSACleanup(); return 0; } 

這就是所謂的阻塞模式。當 WSARecv 函數被調用后,線程將會被掛起,直到遠程端有數據到達或某些系統中斷被觸發,程序自身將不能掌握控制權(除非使用 APC,詳見 WSARecv function)。

Qt 則提供了一個十分友好的編程模式 —— 事件驅動,其實事件驅動早已不是什么新鮮事,GUI 應用必然使用事件驅動,而越來越多服務器應用中也開始采用事件驅動模型(典型的有 Node.js 及其他采用 Reactor 模型的框架)。

我們舉一個簡單的事件驅動的例子,來看這樣一段程序:

int main(int argc, char *argv[]) { QApplication a(argc, argv); QTimer t; QObject::connect(&t, &QTimer::timeout, []() { qDebug() << "Timer fired!"; }); t.start(2000); return a.exec(); } 

你可能會問:“這跟 for-loop + sleep 的方式有什么區別?”嗯,從代碼的層面確實不太好描述它們之間的區別。其實事件驅動與循環結構非常相似,因為它就是一個大循環,不斷從消息隊列中取出消息,然后再分發給事件響應者去處理。

所以一個消息循環可以用下面的偽代碼來表示:

int main() { while (true) { Message msg = GetMessage(); if (msg.isQuitRequest) break; // Process the msg object... } // Clean up here... return 0; } 

看起來也很簡單嘛,沒錯,大致結構就是這樣,但實現細節卻是比較復雜的。

思考這樣一個問題:CPU 處理消息的時間和消息產生的時間哪個比較長?

按現在的 CPU 處理能力來講,消息處理是要遠遠快於消息產生的速度的,試想,你每秒能敲擊幾次鍵盤,手速再快 50 次了不得了吧,但是 CPU 每秒能夠處理的敲擊可能高達幾萬次。如果 CPU 處理完一個消息后,發現沒的消息處理了,接下來可能非常多的 Cycle 后 CPU 仍然撈不着消息處理,這么多 Cycle 就白白浪費了。這就非常像 Mutex 和 Spin Lock 的關系,Spin Lock 只適用於非常短暫的互斥操作,操作時間一長,Spin Lock 就會嚴重消耗 CPU 資源, 因為它就是一個 while 循環,使用不斷 CAS 嘗試獲得鎖。

回到我們上面的消息列隊,GetMessage 這個調用如果每次不管有沒有消息都返回的話,CPU 就永遠閑不下了,每個線程始終 100% 的占用。這顯然是不行的,所以 GetMessage 這個函數不會在沒有消息時返回,相反,它會持續阻塞,直到有消息到達或者 timeout(如果指定了),這樣以來 CPU 在沒有消息的時候就能好好休息幾千上萬個 Cycle 了(線程掛起)。

Qt 的消息分發機制

好了,基本的原理了解了,我們可以回來分析 Qt 了。為了弄明白上面 timer 的例子是怎么回事,我們不妨在輸出語句處加一個斷點,看看它的調用棧:

QMetaObject 往上的部分已經不屬於本文討論的范圍了,因為它屬於 Qt 另一大系統,即 Meta-Object System,我們這里只分析到 QCoreApplication::sendEvent 的位置,因為一旦這個方法被調用了,再往后就沒操作系統和事件機制什么事了。

首先我們從一切的起點,QCoreApplication::exec 開始分析:

int QCoreApplication::exec() { if (!QCoreApplicationPrivate::checkInstance("exec")) return -1; 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 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; } 

threadData 是一個 Thread-Local 變量,每個線程都最多持有一個消息循環,這個方法主要做的就是啟動主線程中的 QEventLoop。繼續分析:

int QEventLoop::exec(ProcessEventsFlags flags)
{
    Q_D(QEventLoop);
    //we need to protect from race condition with QThread::exit
    QMutexLocker locker(&static_cast<QThreadPrivate *>(QObjectPrivate::get(d->threadData->thread))->mutex);
    if (d->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);
            ++d->threadData->loopLevel;
            d->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();
            QEventLoop *eventLoop = d->threadData->eventLoops.pop();
            Q_ASSERT_X(eventLoop == d->q_func(), "QEventLoop::exec()", "internal error");
            Q_UNUSED(eventLoop); // --release warning
            d->inExec = false;
            --d->threadData->loopLevel;
        }
    };
    LoopReference ref(d, locker);

    // remove posted quit events when entering a new event loop
    QCoreApplication *app = QCoreApplication::instance();
    if (app && app->thread() == thread())
        QCoreApplication::removePostedEvents(app, QEvent::Quit);

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

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

這個方法是循環的主體,首先它處理了消息循環嵌套的問題,為什么要嵌套呢?場景可能是這樣的:你想從一個模態窗口中獲取一個用戶的輸入,然后繼續邏輯的執行,如果模態窗口的顯示是異步的,那編程模式就變成 CPS 了,用戶輸入將會觸發一個 callback 進而完成接下來的任務,這在桌面開發中是不太能夠被接受的(C# 玩家請繞行,你們有 await 了不起啊,摔)。如果用嵌套會是一種怎樣的情景呢?需要開模態時再開一個新的 QEventLoop,由於 exec() 方法是阻塞的,在窗口關閉后 exit() 掉這個 event loop 就可以讓當前的方法繼續執行了,同時你也拿到了用戶的輸入。QDialog 的模態就是這樣做的。

Qt 這里使用內部 struct 來實現 try-catch-free 的風格,使用到的就是 C++ 的 RAII,非本文討論范疇,不展開了。

再往下就是一個 while 循環了,在 exit() 方法執行之前,一直循環調用 processEvents() 方法。

processEvents 實現內部是平台相關的,Windows 使用的就是標准的 Windows 消息機制,macOS 上使用的是 CFRunLoop,UNIX 上則是 epoll。本文以 Windows 為例,由於該方法的代碼量較大,本文中就不貼出完整源碼了,大家可以自己查閱 Qt 源碼。概括地說這個方法大體做了以下幾件事:

  1. 初始化一個不可見窗體(下文解釋為什么);
  2. 獲取已經入隊的用戶輸入或 Socket 事件;
  3. 如果 2 中沒有獲取到事件,則執行 PeekMessage,這個函數是非阻塞的,如果有事件則入隊;
  4. 預處理 Posted Event 和 Timer Event;
  5. 處理退出消息;
  6. 如果上述步驟有一步拿到消息了,就使用 TranslateMessage(處理按鍵消息,將 KeyCode 轉換為當前系統設置的相應的字符)+ DispatchMessage 分發消息;
  7. 如果沒有拿到消息,那就阻塞着吧。注意,這里使用的是 MsgWaitForMultipleObjectsEx 這個函數,它除了可以監聽窗體事件以外還能監聽 APC 事件,比 GetMessage 要更通用一些。

下面來說說為什么要創建一個不可見窗體。創建過程如下:

static HWND qt_create_internal_window(const QEventDispatcherWin32 *eventDispatcher) { QWindowsMessageWindowClassContext *ctx = qWindowsMessageWindowClassContext(); if (!ctx->atom) return 0; HWND wnd = CreateWindow(ctx->className, // classname ctx->className, // window name 0, // style 0, 0, 0, 0, // geometry HWND_MESSAGE, // parent 0, // menu handle GetModuleHandle(0), // application 0); // windows creation data. if (!wnd) { qErrnoWarning("CreateWindow() for QEventDispatcherWin32 internal window failed"); return 0; } #ifdef GWLP_USERDATA SetWindowLongPtr(wnd, GWLP_USERDATA, (LONG_PTR)eventDispatcher); #else SetWindowLong(wnd, GWL_USERDATA, (LONG)eventDispatcher); #endif return wnd; } 

在 Windows 中,沒有像 macOS 的 CFRunLoop 那樣比較通用的消息循環,但當你有了一個窗體后,它就幫你在應用與操作系統之間建立了一個 bridge,通過這個窗體你就可以充分利用 Windows 的消息機制了,包括 Timer、異步 Winsock 操作等。同時 Windows API 也允許你綁定一些自定義指針,這樣每個窗體都與 event loop 建立了關系。

接下來 DispatchMessage 的調用會使窗體執行其綁定的 WindowProc 函數,這個函數分別處理 Socket、Notifier、Posted Event 和 Timer。

Posted Event 是一個比較常見的事件類型,它會進而觸發下面的調用:

void QEventDispatcherWin32::sendPostedEvents() { Q_D(QEventDispatcherWin32); QCoreApplicationPrivate::sendPostedEvents(0, 0, d->threadData); } 

在 QCoreApplicaton 中,sendPostedEvents() 方法會循環取出已入隊的事件,這些事件被封裝入 QPostEvent,真實的 QEvent 會被取出再傳入 QCoreApplication::sendEvent() 方法,在此之后的過程就與操作系統無關了。

一般來說,Signals/Slots 在同一線程下會直接調用 QCoreApplication::sendEvent() 傳遞消息,這樣事件就能直接得到處理,不必等待下一次 event loop。而處於不同線程中的對象在 emit signals 之后,會通過 QCoreApplication::postEvent() 來發送消息:

void QCoreApplication::postEvent(QObject *receiver, QEvent *event, int priority) { if (receiver == 0) { qWarning("QCoreApplication::postEvent: Unexpected null receiver"); delete event; return; } QThreadData * volatile * pdata = &receiver->d_func()->threadData; QThreadData *data = *pdata; if (!data) { delete event; return; } data->postEventList.mutex.lock(); while (data != *pdata) { data->postEventList.mutex.unlock(); data = *pdata; if (!data) { delete event; return; } data->postEventList.mutex.lock(); } QMutexUnlocker locker(&data->postEventList.mutex); if (receiver->d_func()->postedEvents && self && self->compressEvent(event, receiver, &data->postEventList)) { return; } if (event->type() == QEvent::DeferredDelete && data == QThreadData::current()) { int loopLevel = data->loopLevel; int scopeLevel = data->scopeLevel; if (scopeLevel == 0 && loopLevel != 0) scopeLevel = 1; static_cast<QDeferredDeleteEvent *>(event)->level = loopLevel + scopeLevel; } QScopedPointer<QEvent> eventDeleter(event); data->postEventList.addEvent(QPostEvent(receiver, event, priority)); eventDeleter.take(); event->posted = true; ++receiver->d_func()->postedEvents; data->canWait = false; locker.unlock(); QAbstractEventDispatcher* dispatcher = data->eventDispatcher.loadAcquire(); if (dispatcher) dispatcher->wakeUp(); } 

事件被加入列隊,然后通過 QAbstractEventDispatcher::wakeUp() 方法喚醒正在被阻塞的 MsgWaitForMultipleObjectsEx 函數:

void QEventDispatcherWin32::wakeUp() { Q_D(QEventDispatcherWin32); d->serialNumber.ref(); if (d->internalHwnd && d->wakeUps.testAndSetAcquire(0, 1)) { // post a WM_QT_SENDPOSTEDEVENTS to this thread if there isn't one already pending PostMessage(d->internalHwnd, WM_QT_SENDPOSTEDEVENTS, 0, 0); } } 

喚醒的方法就是往這個線程所對應的窗體發消息。

 

以上就是 Qt 事件系統的一些底層的原理,雖然本文是相對 Windows 平台,但其他平台的實現也是有很多相通之處的,大家也可以自行研究一下。

 

了解了這些,我們可以做什么呢?我們可以輕松實現類似 Android 中 HandlerThread 那樣的多線程模式。步驟就是:

  1. 創建一個 QThread;
  2. 將需要在新線程中使用的對象(需 QObject 子類,因為要用到 Signals/Slots)移入新線程(QObject::moveToThread());
  3. 使用 Signals/Slots 或 postEvent 觸發對象中的方法。

 

以上。

  • Qt存在事件機制和信號槽機制,為什么要有這兩種機制?只是在不同程度上去解耦以方便用戶使用么

  • Cyandev (作者) 回復江江3 個月前
    事件機制是更高級的抽象,擁有更多特性,比如 accept/ignore,filter,還是實現狀態機等高級 API 的基礎,而信號槽則是一切的基礎,比較底層。

https://zhuanlan.zhihu.com/p/31402358


免責聲明!

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



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