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

Qt 事件系统浅析 (用 Windows API 描述,分析了QCoreApplication::exec()和QEventLoop::exec的源码)(比起新号槽,事件机制是更高级的抽象,拥有更多特性,比如 accept/ignore,filter,还是实现状态机等高级 API 的基础)的更多相关文章

  1. Spring源码分析之IOC的三种常见用法及源码实现(二)

    Spring源码分析之IOC的三种常见用法及源码实现(二) 回顾上文 我们研究的是 AnnotationConfigApplicationContext annotationConfigApplica ...

  2. Linux内核分析(一)---linux体系简介|内核源码简介|内核配置编译安装

    原文:Linux内核分析(一)---linux体系简介|内核源码简介|内核配置编译安装 Linux内核分析(一) 从本篇博文开始我将对linux内核进行学习和分析,整个过程必将十分艰辛,但我会坚持到底 ...

  3. JVM 源码分析(二):搭建 JDK 8 源码调试环境(Windows 上使用 CLion)

    前言 一.准备源码 二.安装 "Bootstrap JDK" 三.配置编译环境 四.编译与测试 五.安装 CMake 和 GDB 五.准备远程调试 六.开始远程调试 前言 上一篇文 ...

  4. Qt源码学习之信号槽

    Qt源码版本 Qt 5.12.0 moc文件 Qt在编译之前会分析源文件,当发现包含了Q_OBJECT宏,则会生成另外一个标准的C++源文件(包含Q_OBJECT宏实现的代码,文件名为moc_+原文件 ...

  5. windows内存体系结构 内存查询,读,写(附录源码)

    “进程内存管理器”这个程序实现的最基本功能也就是对内存的读写,之前的两篇文章也就是做的一个铺垫,介绍了内核模式切换和IoDeviceControl函数进行的应用程序与驱动程序通信的问题.接下来就进入正 ...

  6. Spring源码分析之IOC的三种常见用法及源码实现(一)

    1.ioc核心功能bean的配置与获取api 有以下四种 (来自精通spring4.x的p175) 常用的是前三种 第一种方式 <?xml version="1.0" enc ...

  7. Asp.Net Web Api 2 实现多文件打包并下载文件示例源码_转

    一篇关于Asp.Net Web Api下载文件的文章,之前我也写过类似的文章,请见:<ASP.NET(C#) Web Api通过文件流下载文件到本地实例>本文以这篇文章的基础,提供了Byt ...

  8. Windows Azure NotificationHub+Firebase Cloud Message 实现消息推动(付源码)

    前期项目一直用的是Windows azure NotificationHub+Google Cloud Message 实现消息推送, 但是GCM google已经不再推荐使用,慢慢就不再维护了, 现 ...

  9. 框架源码系列十一:事务管理(Spring事务管理的特点、事务概念学习、Spring事务使用学习、Spring事务管理API学习、Spring事务源码学习)

    一.Spring事务管理的特点 Spring框架为事务管理提供一套统一的抽象,带来的好处有:1. 跨不同事务API的统一的编程模型,无论你使用的是jdbc.jta.jpa.hibernate.2. 支 ...

随机推荐

  1. Linux应急响应(二):捕捉短连接

    0x00 前言 ​ 短连接(short connnection)是相对于长连接而言的概念,指的是在数据传送过程中,只在需要发送数据时,才去建立一个连接,数据发送完成后,则断开此连接,即每次连接只完成一 ...

  2. 【代码审计】后台Getshell的两种常规姿势

    0x00 前言 在早些年刚接触web安全的时候,基础套路都是找注入--找后台--找上传点--找数据库备份--Getshell,然而在代码审计的角度,也存在类似的基本操作. 这里结合代码实例介绍白盒Ge ...

  3. [Vim] Vim 常用基本操作

    1. 导航 1.1. 查看行号 :set number  显示行号 :set number!  隐藏行号 :.=  在底部显示当前行号 :=  在底部显示总行号 1.2. 移动光标 0 或 ^    ...

  4. make: Warning: File `Makefile' has modification time 17 s in the future

    linux下,make makefile文件的时候报警告: make: Warning: File `Makefile' has modification time 17 s in the futur ...

  5. Linux配置示例:配置java环境变量

    1.修改/etc/profile文件   如果你的计算机仅仅作为开发使用时推荐使用这种方法,因为所有用户的shell都有权使用这些环境变量,可能会给系统带来安全性问题. (1)用文本编辑器打开/etc ...

  6. PHP错误 。Parse error: syntax error, unexpected T_INLINE_HTML, expecting T_ENDSWITCH or T_CASE or T_DEFAULT

    If you wan't to use the alternative syntax for switch statements this won't work: <div> <?p ...

  7. 五、K3 WISE 开发插件《K3 Wise 群发短信配置开发(一)之短信平台配置》

    开发环境:K/3 Wise 13.0 目录 一.创建短信数据库 二.配置短信接口 三.设置帐套关键字 四.查询短信余额 一.创建短信数据库 打开帐套管理: 账号默认为Admin,密码不填: 菜单“系统 ...

  8. css方法 - 移动端h5在iphonex的适配

    @media only screen and (device-width:375px) and (device-height:812px) and (-webkit-device-pixel-rati ...

  9. 题目1162:I Wanna Go Home(最短路径问题进阶dijkstra算法))

    题目链接:http://ac.jobdu.com/problem.php?pid=1162 详解链接:https://github.com/zpfbuaa/JobduInCPlusPlus 参考代码: ...

  10. XCode 遇到的问题

    俗话说:工欲善其事必先利其器.抛弃了VS,投入XCode的怀抱.先不说两者的差距,还是先熟悉开发工具是关键.下面列出个人使用中遇到的一些问题. Problem1:修改Xcode字体颜色以及调整字体大小 ...