事件系統在 Qt 中扮演了十分重要的角色,不只 GUI 的方方面面須要使用到事件系統,Signals/Slots 技術也離不開事件系統(多線程間)。咱們本文中暫且不描述 GUI 中的一些特殊狀況,來講說一個非 GUI 應用程序的事件模型。編程
若是讓你寫一個程序,打開一個套接字,接收一段字節而後輸出,你會怎麼作?windows
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 模型的框架)。多線程
咱們舉一個簡單的事件驅動的例子,來看這樣一段程序:app
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 處理消息的時間和消息產生的時間哪一個比較長?oop
按如今的 CPU 處理能力來說,消息處理是要遠遠快於消息產生的速度的,試想,你每秒能敲擊幾回鍵盤,手速再快 50 次了不起了吧,可是 CPU 每秒可以處理的敲擊可能高達幾萬次。若是 CPU 處理完一個消息後,發現沒的消息處理了,接下來可能很是多的 Cycle 後 CPU 仍然撈不着消息處理,這麼多 Cycle 就白白浪費了。這就很是像 Mutex 和 Spin Lock 的關係,Spin Lock 只適用於很是短暫的互斥操做,操做時間一長,Spin Lock 就會嚴重消耗 CPU 資源, 由於它就是一個 while 循環,使用不斷 CAS 嘗試得到鎖。post
回到咱們上面的消息列隊,GetMessage 這個調用若是每次無論有沒有消息都返回的話,CPU 就永遠閒不下了,每一個線程始終 100% 的佔用。這顯然是不行的,因此 GetMessage 這個函數不會在沒有消息時返回,相反,它會持續阻塞,直到有消息到達或者 timeout(若是指定了),這樣以來 CPU 在沒有消息的時候就能好好休息幾千上萬個 Cycle 了(線程掛起)。
好了,基本的原理了解了,咱們能夠回來分析 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 源碼。歸納地說這個方法大致作了如下幾件事:
下面來講說爲何要建立一個不可見窗體。建立過程以下:
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;