事件系统在 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 源码。概括地说这个方法大体做了以下几件事:
- 初始化一个不可见窗体(下文解释为什么);
- 获取已经入队的用户输入或 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->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 那样的多线程模式。步骤就是:
- 创建一个 QThread;
- 将需要在新线程中使用的对象(需 QObject 子类,因为要用到 Signals/Slots)移入新线程(QObject::moveToThread());
- 使用 Signals/Slots 或 postEvent 触发对象中的方法。
以上。
Qt存在事件机制和信号槽机制,为什么要有这两种机制?只是在不同程度上去解耦以方便用户使用么
https://zhuanlan.zhihu.com/p/31402358