事件系统在 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
"
"exceptions from an event handler is not supported in Qt.
"
"You must not let any exception whatsoever propagate through Qt code.
"
"If that is not possible, in Qt 5 you must at least reimplement
"
"QCoreApplication::notify() and catch all exceptions there.
");
}
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 源码。概括地说这个方法大体做了以下几件事:
- 初始化一个不可见窗体(下文解释为什么);
- 获取已经入队的用户输入或 Socket 事件;
- 如果 2 中没有获取到事件,则执行 PeekMessage,这个函数是非阻塞的,如果有事件则入队;
- 预处理 Posted Event 和 Timer Event;
- 处理退出消息;
- 如果上述步骤有一步拿到消息了,就使用 TranslateMessage(处理按键消息,将 KeyCode 转换为当前系统设置的相应的字符)+ DispatchMessage 分发消息;
- 如果没有拿到消息,那就阻塞着吧。注意,这里使用的是 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