Qt源码剖析之事件循环系统(二)—事件循环系统的实现
2025-3-27
| 2025-4-14
Words 4304Read Time 11 min
type
status
date
slug
summary
tags
category
icon
password
《Qt源码剖析之事件循环系统(一)—事件系统》中,简单介绍了 Qt 事件系统的基本设计原理以及使用方法。本文将基于 Qt 6.8 ,从源码的角度分析 Qt 事件循环系统的实现,旨在进一步加深对事件循环的理解。
对于事件循环,我们主要关心两个过程:
  1. 事件的产生和投递
  1. 事件的分发,也就是将事件发送至目的地
接下来,会从一个基础的例子触发,从源码上逐层分析,过程可能有点凌乱,最后总结的时候再从 ”事件投递 —> 分发事件” 这个过程梳理。

1. QCoreApplication::exec()

一个带事件循环的 Qt 应用,其 main() 函数通常如下:
这个 QCoreApplication 也可以是 QApplication, QGuiApplication 等继承自 QCoreApplication 的子类。
Qt 文档告诉我们,QCoreApplication::exec() 运行后,应用进入事件循环。
QCoreApplication::exec() 主要干一件事:创建 QEventLoop 对象,并调用 QEventLoop ::exec() 来启用事件循环。除了 QEventLoop ::exec() 传入了一个 QEventLoop::ApplicationExec 标记,似乎事件循环并不是和 QCoreApplication 绑定的,这也告诉我们可以在其他线程创建事件循环。

2. QEventLoop::exec()

所谓事件循环,就如其名字一般,本质即是一个在一个循环中不断处理事件(processEvents)的过程。
我们接着看。最终的处理是由一个所谓的”事件调度器(eventDispatcher)”进行处理的。
上述代码逻辑非常简单,我们很轻松就能看出,事件循环是和线程相关联的。这个 threadData 是一个 TSL(Thread Local Storage) 变量,简单来说就是每一个线程都拥有一个独立的副本
这里简单提一下:
  1. 主线程的 threadDataQCoreApplication 构造的时候,由 QCoreApplication::init() 在创建事件调度器 createEventDispatcher() 时创建的。其他线程的 threadData 则是在 QThread::start() 时由 QThreadPrivate::start() 创建。
  1. 非主线程的其他线程启动时,在 QThreadPrivate::start() 中调用 QThread::ensureEventDispatcher() 创建事件调度器。从 QEventLoop::processEvents() 的源码中也可以看出,事件调度器也是和线程相关的。
至此,我们能得出一个结论:QThread —> QThreadData —> EventDispatcher 是相关联的,都是 TSL 数据。这意味着事件循环与线程相关,无论事件循环嵌套多少层,都与其所在的线程相关联。

3. QAbstractEventDispatcher::processEvents()

windows 没太研究过,这次基于 linux 进行分析。Linux 平台下的事件调度器通常会创建为 QEventDispatcherUNIXQEventDispatcherGlib ,我选择前者进行分析。
接下来的代码比较长,事件循环的主要逻辑就在此实现,因此不省略代码,简单搂一眼就行。
《Qt源码剖析之事件循环系统(一)—事件系统》的“事件的原理”里提到过,事件循环的主要逻辑是:处理 Posted Event → 处理 spontaneous event。
而这里同样对应了这两个过程。

3.1 处理 Posted Event 队列 (QCoreApplicationPrivate::sendPostedEvents())

这里出现了一个非常熟悉的方法 QCoreApplication::sendEvent() , 在上一章提到,事件可以通过同步和异步的方式发送出去,而同步的方式,正是通过 QCoreApplication::sendEvent() 实现的(不代表 posted event 是同步的方式进行处理的,因为经过了事件循环,因此是异步)。
QCoreApplication::sendEvent() 最终的处理是将事件 event 传入 “事件处理链”,在该链中,如果事件被处理则提前返回,不再往下传递。
同时,通过 QThreadData::requiresCoreApplication 标志(默认为true),以及当前线程有没有 QThreadData(当前线程有没有启动),来决定 “事件处理链” 经不经过 QCoreApplication::notify()
注:除非强行将 QThreadData::requiresCoreApplication 标志置为 false,否则 “this function is called for all events sent to any object in any thread”(qt文档原话)
什么是事件处理链?
在上一章的第4节中提到过“Qt中的事件可以在五个不同的层级上进行处理”,这个”事件处理链“,从起点到终点分别对应这个五个层级的处理器(handler)。
源码上也能清楚看出:
大部分情况下,该事件处理链为:
QCoreApplication::notify()QCoreApplication::eventFilter()QObject::eventFilter()(QObject*)receiver->event()“specal event method”
在使用的时候,我们可以按需求重写事件处理链上的任一处理器,对感兴趣的事件进行处理,并且可以决定是否截拦事件,是否允许事件往下传递,对于不感兴趣的事件,可以调用父类的初始处理器。

3.2 处理自发事件(spontaneous event)

自发事件,我理解为定时器事件、socket事件。
首先,事件调度器会根据传入的 flags 判断当前是否需要监听定时器事件和socket事件。这里我们着重分析socket事件,定时器其实也是类似做法。
太长不看系列:其实就是 poll() /ppoll() 监听感兴趣的 socket 事件,并将 socket 事件响应封装成 ”sent event“直接发送到事件通知器(notifier)中, 这里的 notifier 为用户创建的 QSocketNotifier ,用户创建时可绑定 socket 文件描述符以及感兴趣的事件。
为什么说 socket 事件是一种自发事件,因为 socket 事件是由系统响应的,我们能做的只有监听。而 posted event 和 sent event 是应用产生的,我们可以向事件循环投递事件,以此作为一种事件响应。
学过 linux 网络编程的看到这个应该比较熟悉,其实这就是 I/O复用 中的 poll() /ppoll() 系统调用。
简单来说就是:
  1. 将感兴趣的 socket 事件存储再一个 pollfd 数组中。
  1. 将 pollfd 传递到 poll() /ppoll() 系统调用中。
  1. 根据 poll() /ppoll() 的返回值,获取事件发生的数量,同时 pollfd 数组也作为输出,其中的 pollfd::revents 为某文件描述符实际发生的事件(可读、可写、异常等)。
而这些都被 Qt 封装成了 QSocketNotifier 类。其大致用法流程如下:
  1. 创建 QSocketNotifier 对象,并绑定感兴趣的 socket 文件描述符以及对应感兴趣的事件。
2. 通过 QSocketNotifier::setEnable() 将其添加到所在线程的”事件调度器”中进行监听。
  1. 事件发生后,会将响应的事件封装成 QEvent::SockAct 类型的事件,并通过 QCoreApplication::sendEvent()发送到 ”事件处理链” 中。
  1. 如果没被处理,最后则在 QSocketNotifier::event() 中,向外发射 activated 信号。
  1. 用户通过信号槽机制响应 activated 信号并处理。

3.3 小结

可能到这里已经有点乱了。。。但其实不妨抓住重点。第 3 节是事件循环的主要处理逻辑,也是《Qt源码剖析之事件循环系统(一)—事件系统》中第 1 节 ”事件的原理“ 的详解。
当前的目标是分析三种事件:自发事件(spontaneous event)被发布事件(posted event)被发送事件(sent event) 的处理方式。
 
我们前面提到过 QCoreApplication::sendEvent() 是以同步的方式将事件推送出去,请牢记。
 
那么这三种事件的处理方式的共同点是什么?
  • 通过总结,其实可以看出事件调度的核心是 ”事件处理链“,这条 “链” 在用户层提供了不同优先级的事件处理接口
  • 而将事件传入 ”事件处理链“ 的方法即是 QCoreApplication::sendEvent() , 事实上上述三种事件最终都是通过这个方法传入 “事件处理链” 的。
 
那三种事件处理的不同点是什么?
  • 自发事件(spontaneous event):在事件循环内,为异步操作。其使用 poll 系统调用监听一组事件(可以看作一个队列),事件发生后,依次调用 QCoreApplication::sendEvent(receiver, event) 将事件向应用层推送(receiver对应监听的QSocketNotifier)。
    • 注:这个过程在事件循环内是同步的,因此最好不用直接在”事件处理链”中使用耗时操作处理自发事件,防止阻塞事件循环。正确的处理方式是连接 QSocketNotifier::activated 信号,通过信号槽机制将同步转化为异步的 被发布事件(posted event)。
       
  • 被发布事件(posted event):在事件循环内,为异步操作。其所在线程维护一个队列 postEventList (QList<QPostEvent>),QPostEvent 内部维护一个 receiver 和一个 QEvent , 并通过 QCoreApplication::sendEvent(receiver, event) 向应用层推送。
    • 注:被发布事件(posted event) 通过 QCoreApplication::postEvent() 方法将事件投递到 receiverpostEventList 中。
       
  • 被发送事件(sent event) :直接调用 QCoreApplication::sendEvent(receiver, event) 向应用层推送,不经过事件循环处理。为同步操作。
 
  • 自发事件(spontaneous event) 和 被发布事件(posted event) 均在事件循环内被处理,形式上属于异步。被发送事件(sent event) 直接调用 QCoreApplication::sendEvent(receiver, event) 不在事件循环内,形式上属于同步。
    •  

3.4 QThreadPipe

在事件调度器中还出现了一个比较有意思的东西,看名字就知道,这是一个类似于“管道”的东西,管道一般是用来做线程间通讯或者进程间通讯的。
在这里是用作唤醒线程的。这里简单介绍一下用法和原理。
  1. 初始化时创建一个无阻塞的事件文件描述符 fds[0]
  1. prepare() 创建一个监听该事件文件描述符的可读事件的 pollfd
  1. wakeUp() 对事件文件描述符fds[0]写入 1,此时可读事件发生。
  1. 线程在无事件处理时,可以睡眠,直到监听的文件描述符有可读事件发生(被唤醒)。
    1. 可以在其他地方通过 QThreadPipe::wakeUp() 直接唤醒线程,即使没事件需要处理。
总结: 一个线程通过 prepare() 获取可读 pollfd 并监听。另一个线程通过 wakeUp() 写入事件值。这样第一线程就能被唤醒了。

4. 总结

本文自顶而下地分析了Qt事件循环的源码,并在次过程中找到《Qt源码剖析之事件循环系统(一)—事件系统》中介绍的三种事件对应的事件的添加以及分发方式,从代码上分析了“事件处理链”。
由于 Qt 的代码并不简单,很多细枝末节的东西就直接省略不分析了。其中还是有很多比较有意思的设计和实现的。目前进行的 Qt 源码分析系列仅制作了两个主题,也是第一次花时间认真写。一直以来,我都觉得分析一些优秀的源码对编码能力有较大的帮助,但目前来看对”看源码”这件事还是缺少经验和技巧,有点像总结流程图、写流水账。总的来说希望之后能做出更好的源码分析,慢慢进步吧。
 
Qt源码剖析之事件循环系统(一)—事件系统Qt源码剖析之信号槽机制(一)—信号槽机制的实现
Loading...