事件系统在 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;