type
status
date
slug
summary
tags
category
icon
password
在 《Qt源码剖析之事件循环系统(一)—事件系统》中,简单介绍了 Qt 事件系统的基本设计原理以及使用方法。本文将基于 Qt 6.8 ,从源码的角度分析 Qt 事件循环系统的实现,旨在进一步加深对事件循环的理解。
对于事件循环,我们主要关心两个过程:
- 事件的产生和投递
- 事件的分发,也就是将事件发送至目的地
接下来,会从一个基础的例子触发,从源码上逐层分析,过程可能有点凌乱,最后总结的时候再从 ”事件投递 —> 分发事件” 这个过程梳理。
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) 变量,简单来说就是每一个线程都拥有一个独立的副本。这里简单提一下:
- 主线程的
threadData
在QCoreApplication
构造的时候,由QCoreApplication::init()
在创建事件调度器createEventDispatcher()
时创建的。其他线程的threadData
则是在QThread::start()
时由QThreadPrivate::start()
创建。
- 非主线程的其他线程启动时,在
QThreadPrivate::start()
中调用QThread::ensureEventDispatcher()
创建事件调度器。从QEventLoop::processEvents()
的源码中也可以看出,事件调度器也是和线程相关的。
至此,我们能得出一个结论:QThread —> QThreadData —> EventDispatcher 是相关联的,都是 TSL 数据。这意味着事件循环与线程相关,无论事件循环嵌套多少层,都与其所在的线程相关联。
3. QAbstractEventDispatcher::processEvents()
windows 没太研究过,这次基于 linux 进行分析。Linux 平台下的事件调度器通常会创建为
QEventDispatcherUNIX
或 QEventDispatcherGlib
,我选择前者进行分析。接下来的代码比较长,事件循环的主要逻辑就在此实现,因此不省略代码,简单搂一眼就行。
在 《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()
系统调用。简单来说就是:
- 将感兴趣的 socket 事件存储再一个 pollfd 数组中。
- 将 pollfd 传递到
poll() /ppoll()
系统调用中。
- 根据
poll() /ppoll()
的返回值,获取事件发生的数量,同时 pollfd 数组也作为输出,其中的 pollfd::revents 为某文件描述符实际发生的事件(可读、可写、异常等)。
而这些都被 Qt 封装成了
QSocketNotifier
类。其大致用法流程如下:- 创建
QSocketNotifier
对象,并绑定感兴趣的 socket 文件描述符以及对应感兴趣的事件。
2. 通过
QSocketNotifier::setEnable()
将其添加到所在线程的”事件调度器”中进行监听。- 事件发生后,会将响应的事件封装成
QEvent::SockAct
类型的事件,并通过QCoreApplication::sendEvent()
发送到 ”事件处理链” 中。
- 如果没被处理,最后则在
QSocketNotifier::event()
中,向外发射activated
信号。
- 用户通过信号槽机制响应
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()
方法将事件投递到 receiver
的 postEventList
中。- 被发送事件(sent event) :直接调用
QCoreApplication::sendEvent(receiver, event)
向应用层推送,不经过事件循环处理。为同步操作。
- 自发事件(spontaneous event) 和 被发布事件(posted event) 均在事件循环内被处理,形式上属于异步。被发送事件(sent event) 直接调用
QCoreApplication::sendEvent(receiver, event)
不在事件循环内,形式上属于同步。
3.4 QThreadPipe
在事件调度器中还出现了一个比较有意思的东西,看名字就知道,这是一个类似于“管道”的东西,管道一般是用来做线程间通讯或者进程间通讯的。
在这里是用作唤醒线程的。这里简单介绍一下用法和原理。
- 初始化时创建一个无阻塞的事件文件描述符
fds[0]
。
prepare()
创建一个监听该事件文件描述符的可读事件的pollfd
。
wakeUp()
对事件文件描述符fds[0]
写入 1,此时可读事件发生。
- 线程在无事件处理时,可以睡眠,直到监听的文件描述符有可读事件发生(被唤醒)。
可以在其他地方通过
QThreadPipe::wakeUp()
直接唤醒线程,即使没事件需要处理。总结: 一个线程通过
prepare()
获取可读 pollfd 并监听。另一个线程通过 wakeUp()
写入事件值。这样第一线程就能被唤醒了。4. 总结
本文自顶而下地分析了Qt事件循环的源码,并在次过程中找到《Qt源码剖析之事件循环系统(一)—事件系统》中介绍的三种事件对应的事件的添加以及分发方式,从代码上分析了“事件处理链”。
由于 Qt 的代码并不简单,很多细枝末节的东西就直接省略不分析了。其中还是有很多比较有意思的设计和实现的。目前进行的 Qt 源码分析系列仅制作了两个主题,也是第一次花时间认真写。一直以来,我都觉得分析一些优秀的源码对编码能力有较大的帮助,但目前来看对”看源码”这件事还是缺少经验和技巧,有点像总结流程图、写流水账。总的来说希望之后能做出更好的源码分析,慢慢进步吧。