Qt 信号槽、事件循环与线程通信:从使用到源码
本文从普通 C++ 回调和消息队列开始,逐步讲解 Qt 信号槽的源码实现、事件循环如何等待和分发事件、事件过滤器的调用顺序,以及 QObject 在线程之间如何安全通信。
全文重点回答:
connect()建立连接时,Qt 内部保存了什么?emit之后为什么能找到一个或多个槽?- Direct、Queued 和 BlockingQueued 在源码中从哪里分叉?
- 排队调用如何复制参数,并在接收线程恢复成真实函数调用?
app.exec()到底是不是一个简单的while循环?- posted event、系统消息、定时器、socket notifier 到底谁先处理?
- 事件过滤器、
notify()、event()和专用事件函数的先后顺序是什么? - 为什么 UI 卡死、取消不响应、事件积压和嵌套循环重入经常同时出现?
阅读目标:即使此前只会普通 C++,也能从手写回调一路推导到 Qt 源码中的连接表、
QMetaObject::activate()、QMetaCallEvent、postEvent()和平台事件分发器,并能根据线程、时序、生命周期和吞吐量选择正确方案。
0. 阅读前先建立四个直觉
0.1 信号槽首先是一套"受管理的回调系统"
最简单的回调:
cpp
void onProgress(int value)
{
std::cout << value << '\n';
}
void download(void (*callback)(int))
{
callback(10);
callback(50);
callback(100);
}
这里已经有信号槽最基本的思想:
text
下载器产生变化
↓
回调函数被通知
但它只能保存一个回调,无法自然解决:
- 一个信号通知多个接收者。
- 接收者对象销毁后自动停止调用。
- 成员函数、lambda 和普通函数统一表示。
- 同一对象有多个不同信号。
- 跨线程时把调用排队到接收线程。
- 回调执行期间新增或删除连接。
Qt 信号槽是在"回调"基础上补齐这些工程能力。
0.2 排队信号槽首先是一条"带类型信息的任务消息"
直接调用:
cpp
receiver->handleResult(result);
要求当前调用栈立刻执行。
排队调用可以想成先制作一张任务单:
text
目标对象:receiver
目标函数:handleResult
参数副本:result
发送者:sender
信号编号:resultReady
把任务单放入接收线程队列,目标线程稍后取出并执行。
Qt 内部的 QMetaCallEvent 就承担类似角色:
text
排队信号槽
= 一次函数调用
+ 参数副本
+ 目标对象
+ 被包装成事件
0.3 事件循环不是不停空转,而是"处理完后等待"
错误想象:
cpp
while (true) {
checkEverythingRepeatedly(); // 持续空转,CPU 100%
}
实际事件循环更接近:
cpp
while (!exitRequested) {
processReadyWork();
if (nothingIsReady())
sleepUntilOsTimerSocketOrPostedEventWakesUs();
}
等待通常由操作系统机制完成:
- Windows 消息队列和等待 API。
- Unix/Linux 的
poll、事件文件描述符和唤醒管道。 - macOS/iOS 的原生 run loop。
所以一个空闲 Qt 程序不会因为事件循环存在就持续占满 CPU。
0.4 "处理顺序"必须分三层讨论
问"Qt 事件处理顺序是什么"时,不能只给一个绝对列表。需要区分:
- 同一个 posted-event 队列内部
- 高优先级先处理。
- 相同优先级按投递顺序处理。
- 一个事件交给对象后的分发顺序
- 应用级过滤器。
- 对象级过滤器。
receiver->event()。- 专用事件处理函数。
- 不同事件来源之间
- posted events。
- 操作系统窗口消息。
- 定时器。
- socket notifier。
- 平台唤醒消息。
第三层没有一个对所有平台、所有 Qt 版本都完全相同的总顺序。应用代码不应依赖"定时器永远先于鼠标"或"queued slot 永远先于 socket notifier"这类假设。
0.5 先手写一个最小信号系统
第一步:保存多个回调
cpp
#include <functional>
#include <vector>
class IntSignal
{
public:
using Slot = std::function<void(int)>;
void connect(Slot slot)
{
m_slots.push_back(std::move(slot));
}
void emitSignal(int value)
{
for (auto &slot : m_slots)
slot(value);
}
private:
std::vector<Slot> m_slots;
};
使用:
cpp
IntSignal progressChanged;
progressChanged.connect([](int value) {
std::cout << "UI: " << value << '\n';
});
progressChanged.connect([](int value) {
std::cout << "log: " << value << '\n';
});
progressChanged.emitSignal(50);
这已经实现:
- 一对多。
- lambda 接收。
- 发送者不关心接收者是谁。
第二步:连接为什么不能只存函数
如果接收者是对象:
cpp
class Label
{
public:
void setValue(int value);
};
连接至少要保存:
text
调用哪个对象
调用哪个函数
概念记录:
cpp
struct Connection
{
void *receiver;
std::function<void(void *, int)> invoke;
};
Qt 的真实连接记录还需要保存:
- sender。
- signal index。
- receiver/context。
- slot 的方法索引或 functor 包装。
- connection type。
- 接收者线程数据。
- 参数元类型。
- single-shot 等标志。
- 链表关系和连接 ID。
第三步:为什么要有反向关系
只在发送者中保存连接,会遇到问题:
text
接收者先析构
发送者仍保存接收者地址
下次 emit 调用悬垂指针
所以连接系统还需要让接收者知道"哪些发送者正在连接我",析构时才能清理相关连接。
可以把连接看成一条有两个端点的边:
text
sender -- Connection -- receiver
发送者侧用于发信号时快速查找;接收者侧用于析构时快速断开。
第四步:异步连接需要任务队列
最简任务队列:
cpp
#include <functional>
#include <mutex>
#include <queue>
class TaskQueue
{
public:
void post(std::function<void()> task)
{
std::lock_guard lock(m_mutex);
m_tasks.push(std::move(task));
}
bool processOne()
{
std::function<void()> task;
{
std::lock_guard lock(m_mutex);
if (m_tasks.empty())
return false;
task = std::move(m_tasks.front());
m_tasks.pop();
}
task();
return true;
}
private:
std::mutex m_mutex;
std::queue<std::function<void()>> m_tasks;
};
排队发送:
cpp
queue.post([receiver, result] {
receiver->handleResult(result);
});
这段代码揭示排队调用的三个关键要求:
receiver执行时必须仍然有效。result必须脱离发送者当前栈保存下来。- 队列必须唤醒目标线程处理任务。
Qt 的排队连接正是在更复杂的对象生命周期、元类型和平台事件循环上实现这三点。
0.6 手写版本还缺什么
上面的教学代码仍然没有解决:
- 连接期间的线程安全。
- 接收对象析构自动断连。
- 回调执行时断开自身。
- 信号递归发射。
- 连接期间又新增连接。
- 参数类型擦除后的构造和析构。
- 优先级和事件压缩。
- BlockingQueued 的等待和死锁检测。
- 平台事件、timer、socket 与任务队列的统一等待。
这些正是后文阅读 Qt 源码时要关注的内容。
1. 先建立整体模型
Qt 中常被混在一起的几个概念,其职责并不相同:
| 概念 | 主要职责 |
|---|---|
| 信号槽 | 建立对象之间的一对多通知和调用关系 |
| 元对象系统 | 描述信号、槽、属性和参数类型,提供动态分发基础 |
| 事件 | 表示一次待处理的输入、定时器、绘制、删除或自定义消息 |
| 事件循环 | 从队列和平台事件源取出事件并分发 |
| 线程亲和性 | 决定 QObject 的排队调用和事件在哪个线程处理 |
QThread |
管理一个操作系统线程,可在其中运行事件循环 |
它们的关系可以概括为:
#mermaid-svg-7pBw1HHacHj1WiHX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7pBw1HHacHj1WiHX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7pBw1HHacHj1WiHX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7pBw1HHacHj1WiHX .error-icon{fill:#552222;}#mermaid-svg-7pBw1HHacHj1WiHX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7pBw1HHacHj1WiHX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7pBw1HHacHj1WiHX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7pBw1HHacHj1WiHX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7pBw1HHacHj1WiHX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7pBw1HHacHj1WiHX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7pBw1HHacHj1WiHX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7pBw1HHacHj1WiHX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7pBw1HHacHj1WiHX .marker.cross{stroke:#333333;}#mermaid-svg-7pBw1HHacHj1WiHX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7pBw1HHacHj1WiHX p{margin:0;}#mermaid-svg-7pBw1HHacHj1WiHX .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7pBw1HHacHj1WiHX .cluster-label text{fill:#333;}#mermaid-svg-7pBw1HHacHj1WiHX .cluster-label span{color:#333;}#mermaid-svg-7pBw1HHacHj1WiHX .cluster-label span p{background-color:transparent;}#mermaid-svg-7pBw1HHacHj1WiHX .label text,#mermaid-svg-7pBw1HHacHj1WiHX span{fill:#333;color:#333;}#mermaid-svg-7pBw1HHacHj1WiHX .node rect,#mermaid-svg-7pBw1HHacHj1WiHX .node circle,#mermaid-svg-7pBw1HHacHj1WiHX .node ellipse,#mermaid-svg-7pBw1HHacHj1WiHX .node polygon,#mermaid-svg-7pBw1HHacHj1WiHX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7pBw1HHacHj1WiHX .rough-node .label text,#mermaid-svg-7pBw1HHacHj1WiHX .node .label text,#mermaid-svg-7pBw1HHacHj1WiHX .image-shape .label,#mermaid-svg-7pBw1HHacHj1WiHX .icon-shape .label{text-anchor:middle;}#mermaid-svg-7pBw1HHacHj1WiHX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7pBw1HHacHj1WiHX .rough-node .label,#mermaid-svg-7pBw1HHacHj1WiHX .node .label,#mermaid-svg-7pBw1HHacHj1WiHX .image-shape .label,#mermaid-svg-7pBw1HHacHj1WiHX .icon-shape .label{text-align:center;}#mermaid-svg-7pBw1HHacHj1WiHX .node.clickable{cursor:pointer;}#mermaid-svg-7pBw1HHacHj1WiHX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7pBw1HHacHj1WiHX .arrowheadPath{fill:#333333;}#mermaid-svg-7pBw1HHacHj1WiHX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7pBw1HHacHj1WiHX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7pBw1HHacHj1WiHX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7pBw1HHacHj1WiHX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7pBw1HHacHj1WiHX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7pBw1HHacHj1WiHX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7pBw1HHacHj1WiHX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7pBw1HHacHj1WiHX .cluster text{fill:#333;}#mermaid-svg-7pBw1HHacHj1WiHX .cluster span{color:#333;}#mermaid-svg-7pBw1HHacHj1WiHX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7pBw1HHacHj1WiHX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7pBw1HHacHj1WiHX rect.text{fill:none;stroke-width:0;}#mermaid-svg-7pBw1HHacHj1WiHX .icon-shape,#mermaid-svg-7pBw1HHacHj1WiHX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7pBw1HHacHj1WiHX .icon-shape p,#mermaid-svg-7pBw1HHacHj1WiHX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7pBw1HHacHj1WiHX .icon-shape .label rect,#mermaid-svg-7pBw1HHacHj1WiHX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7pBw1HHacHj1WiHX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7pBw1HHacHj1WiHX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7pBw1HHacHj1WiHX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Direct
Queued
发送信号
连接类型
当前线程立即调用槽
复制参数并创建元调用事件
投递到接收对象所属线程
系统输入/定时器/网络
postEvent/deleteLater
目标线程事件循环
notify / eventFilter / event
调用槽或事件处理函数
关键结论:
- 信号不天然异步。
- 槽不天然运行在接收对象线程。
- 只有排队连接才借助目标线程事件循环。
- 没有运行事件循环,排队槽、定时器和普通投递事件就不会按预期处理。
2. 信号和槽分别是什么
2.1 信号
信号表示"对象发生了值得外部关注的状态变化":
cpp
class Downloader : public QObject
{
Q_OBJECT
signals:
void progressChanged(qint64 received, qint64 total);
void finished(QByteArray data);
void failed(QString reason);
};
设计信号时应表达事实,而不是命令:
cpp
// 更像状态事实
emit connectionLost(reason);
// 名称像在命令未知接收者做事,耦合更重
emit showErrorDialogNow(reason);
信号的特点:
- 可以没有接收者。
- 可以连接多个槽。
- 可以连接另一个信号,实现信号转发。
- 通常只应由定义它的类及其派生类发出。
- 函数体由
moc生成,不应手工实现。
2.2 槽
槽是可以作为信号接收目标的函数:
cpp
public slots:
void cancel();
private slots:
void handleChunk(const QByteArray &chunk);
槽本质上仍是普通 C++ 成员函数:
cpp
downloader.cancel(); // 完全合法的直接函数调用
在现代函数指针语法中,接收端甚至不一定标记为 slots,普通成员函数也能作为目标:
cpp
connect(
lineEdit,
&QLineEdit::textChanged,
label,
&QLabel::setText);
声明成槽的额外意义主要是让该方法进入元对象方法表,从而支持旧字符串连接、动态查询和动态调用。
3. 信号槽有哪几种写法
"信号槽有几种"可能指连接语法,也可能指连接类型。这里分别说明。
3.1 成员函数指针语法
cpp
QMetaObject::Connection connection = connect(
sender,
&Sender::valueChanged,
receiver,
&Receiver::setValue);
优点:
- 编译期检查类型兼容性。
- 支持 IDE 重构和跳转。
- 无需运行期字符串查找。
- 返回连接句柄,便于精确断开。
这是新代码的首选。
3.2 lambda / functor
cpp
connect(
button,
&QPushButton::clicked,
this,
[this] {
saveDocument();
});
建议始终提供 context 对象,例如上面的 this:
- context 销毁时自动断开。
- 排队调用会进入 context 所属线程。
- 比无 context 的 lambda 更容易控制捕获对象的生命周期。
危险写法:
cpp
QObject *temporary = createObject();
connect(sender, &Sender::ready, [temporary] {
temporary->doWork(); // temporary 若提前销毁,这里会悬垂
});
更稳妥的写法:
cpp
connect(sender, &Sender::ready, temporary, [temporary] {
temporary->doWork();
});
context 只能保证连接随 context 断开,lambda 捕获的其他裸指针仍需要各自的生命周期保证。必要时使用 QPointer<T> 检测 QObject 是否已经销毁。
3.3 旧字符串语法
cpp
connect(
sender,
SIGNAL(valueChanged(int)),
receiver,
SLOT(setValue(int)));
适用场景:
- 接口名称只能在运行期得到。
- 与遗留 Qt 代码交互。
- 某些高度动态的插件框架。
缺点:
- 类型错误通常只在运行期警告。
- 重构工具难以可靠修改字符串。
- 参数名不能写入签名。
- 查找和规范化发生在运行期。
3.4 信号连接信号
cpp
connect(
internalWorker,
&Worker::failed,
this,
&Controller::failed);
这适合对内部组件的信号重新命名或向外转发。
3.5 动态调用
不建立长期连接,只排队执行一次:
cpp
QMetaObject::invokeMethod(
worker,
&Worker::refresh,
Qt::QueuedConnection);
它适合"把这个动作安排到对象所属线程",不适合替代所有普通函数调用。
4. 连接类型:四种基础类型和两个标志
Qt::ConnectionType 决定槽在哪里、何时执行。
4.1 Qt::AutoConnection
默认类型。
规则在信号发出时判断:
- 如果接收对象生活在当前发信号线程,按直接连接执行。
- 否则按排队连接执行。
注意判断的是:
- 当前正在执行
emit的线程。 - 接收对象的线程亲和性。
不是简单比较"sender 对象属于哪个线程"。一个对象的方法可能被错误地从其他线程直接调用并发出信号,此时发信线程就是那个调用线程。
适用:
- 大多数正常的对象间连接。
- 对象未来可能
moveToThread(),希望行为随实际线程关系变化。
4.2 Qt::DirectConnection
信号发出时立即调用槽:
text
emit 所在线程 == 槽执行线程
它类似带连接管理的普通函数调用:
- 不需要事件循环。
emit后面的代码会等待槽返回。- 多个槽按连接建立顺序依次执行。
- 槽抛出的异常不应穿越 Qt 事件边界;Qt 工程通常在槽内部处理异常。
跨线程强制 DirectConnection 很危险,因为槽会在发信线程执行,而不是接收对象所属线程。若槽访问对象状态,可能与该对象线程中的事件处理并发,产生数据竞争。
适用:
- 明确处于同一线程。
- 调用必须同步完成。
- 已经自行建立完整同步协议的少数底层场景。
4.3 Qt::QueuedConnection
信号发出时不会立即执行槽,而是:
- 读取连接和参数元类型。
- 复制或封送参数。
- 创建一次元调用事件。
- 投递到接收对象所属线程的事件队列。
- 发信线程继续执行。
- 目标线程事件循环稍后取出事件并调用槽。
text
槽执行线程 == receiver->thread()
适用:
- 跨线程通信的默认选择。
- 同一线程中希望推迟到下一轮事件循环执行。
- 避免当前调用栈继续递归。
前提:
- 接收对象线程必须有运行中的事件循环。
- 参数类型必须能被 Qt 元类型系统处理。
- 参数在排队时通常会形成独立副本,不能依赖原始引用稍后仍然有效。
排队参数为什么不能随便用引用
声明可以使用 const T &:
cpp
signals:
void dataReady(const QByteArray &data);
对于排队连接,Qt 会依据元类型复制值,而不是把发送者栈上的引用原样留到未来。真正需要警惕的是值内部携带的非拥有资源:
cpp
struct BufferView
{
const char *data; // 不拥有内存
qsizetype size;
};
即使 BufferView 本身可复制,内部指针也可能在槽执行前失效。跨线程消息应优先使用:
QByteArray、QString、QImage等具有清晰值语义的类型。- 自定义拥有型值类型。
QSharedPointer<const T>/std::shared_ptr<const T>形式的不可变共享数据。
4.4 Qt::BlockingQueuedConnection
行为与排队连接类似,但发信线程会阻塞,直到目标线程执行完槽。
适用场景非常有限,例如:
- 必须同步取得另一个线程中对象的结果。
- 已明确证明线程之间不存在环形等待。
- 调用频率低且目标线程一定在处理事件。
严重风险:
- 接收对象和发信线程相同时必然死锁。
- 目标线程等待发信线程持有的锁时死锁。
- 目标线程事件循环被阻塞时,发信线程无限等待。
- 双向 BlockingQueued 调用形成环路时死锁。
不要在持有业务互斥锁时使用它:
cpp
QMutexLocker locker(&mutex);
emit requestToOtherThread(); // 对方槽若也要 mutex,会死锁
更好的设计通常是:
- 请求信号 + 结果信号。
- future/promise。
- 状态机。
- 将需要同步访问的数据移动到同一执行上下文。
4.5 Qt::UniqueConnection
这是附加标志,不是独立的调度方式:
cpp
connect(
sender,
&Sender::ready,
receiver,
&Receiver::onReady,
Qt::AutoConnection | Qt::UniqueConnection);
作用:
- 相同 sender、signal、receiver、member slot 的重复连接会失败。
connect()返回无效连接句柄。
限制:
- 对 lambda、非成员函数和一般 functor 不起唯一性判断作用。
- 它不能替代清晰的连接生命周期管理。
4.6 Qt::SingleShotConnection
Qt 6 提供的一次性连接标志:
cpp
connect(
sender,
&Sender::ready,
receiver,
&Receiver::consumeOnce,
Qt::QueuedConnection | Qt::SingleShotConnection);
槽只触发一次,连接随后自动断开。
适合:
- 一次性初始化完成通知。
- 只等待第一次状态满足。
- 替代"在槽中保存句柄并手动断开"的样板代码。
4.7 连接类型总结
| 类型/标志 | 槽执行线程 | 是否立即 | 是否要求事件循环 | 主要风险 |
|---|---|---|---|---|
AutoConnection |
同线程则当前线程,否则接收线程 | 自动判断 | 跨线程时需要 | 错误理解发信线程 |
DirectConnection |
发信线程 | 是 | 否 | 跨线程数据竞争 |
QueuedConnection |
接收对象线程 | 否 | 是 | 无事件循环、参数生命周期 |
BlockingQueuedConnection |
接收对象线程 | 否,发送方等待 | 是 | 死锁 |
UniqueConnection |
取决于组合的基础类型 | - | - | 对 lambda 不生效 |
SingleShotConnection |
取决于组合的基础类型 | - | - | 只处理第一次触发 |
5. connect() 内部大致做了什么
这一章结合 Qt 6 qtbase/src/corelib/kernel/qobject.cpp 的实现讲解。
需要先划清边界:
QObject::connect()、各种连接类型和线程语义是公开行为,可以依赖。QObjectPrivate::Connection、doActivate()、queued_activate()、QQueuedMetaCallEvent等属于源码实现细节。- 私有类名、字段和容器结构可能随版本变化。
- 阅读源码的目的,是理解因果链和排障,不是在业务代码中调用私有 API。
5.1 一条连接究竟是什么
代码:
cpp
connect(
sender,
&Sender::valueChanged,
receiver,
&Receiver::setValue,
Qt::AutoConnection);
不是把 sender 和 receiver 神秘地绑在一起,而是创建一条连接记录。
概念结构:
cpp
struct ConnectionConcept
{
QObject *sender;
int signalIndex;
QObject *receiver;
Callable slot;
Qt::ConnectionType type;
ThreadData *receiverThreadData;
MetaTypeList argumentTypes;
bool singleShot;
};
Qt 6 私有实现中的连接记录大致保存:
text
sender
signal_index
receiver
receiverThreadData
connectionType
method_offset / method_relative / callFunction
或者 slotObj
argumentTypes
isSingleShot
连接链表指针
连接 ID
为什么同时可能有"方法索引"和"slot object"?
- 传统元对象槽可以用方法索引和静态元调用函数定位。
- lambda、functor、成员函数指针会被模板封装成统一的 slot object。
无论外部写法如何,发信号时都需要变成"可以统一调用的目标"。
5.2 connect() 的公开模板层做什么
现代成员函数指针连接首先在头文件模板中完成编译期工作:
text
检查 sender 是否匹配信号所属类
检查 signal 是否真的是信号
检查槽参数能否接收信号参数
处理重载和参数数量
把成员函数/lambda 封装成统一可调用对象
所以这类错误在编译期暴露:
cpp
connect(
sender,
&Sender::textChanged, // 参数是 QString
receiver,
&Receiver::setCount); // 参数是完全不兼容的业务类型
模板层完成类型检查后,会进入非模板的内部连接实现。这样可以避免为所有类型组合复制大量底层连接管理代码。
5.3 如何把信号成员函数变成 signal index
传入的是:
cpp
&Sender::valueChanged
内部连接表更适合保存整数:
text
signal index = 2
Qt 会借助 moc 生成的静态元调用能力,把成员函数指针映射到信号索引。概念过程:
text
成员函数指针 &Sender::valueChanged
↓
沿 Sender 的 QMetaObject 继承链查询
↓
得到相对于当前元对象的信号位置
↓
加上父类 signal offset
↓
得到在整个对象信号空间中的索引
索引让发送者可以直接定位:
text
这个信号对应的连接列表
而不必每次发射都比较方法名称字符串。
5.4 UniqueConnection 在建立连接时检查
如果连接类型带:
cpp
Qt::UniqueConnection
内部会先遍历当前信号已有的连接,检查是否已经存在相同的:
text
sender
signal
receiver
member slot
存在则返回无效连接,不再分配新记录。
为什么它通常不适用于一般 lambda?
cpp
[this] { update(); }
两个语法完全相同的 lambda 表达式,也可能是两个不同闭包对象,捕获状态也可能不同。Qt 很难定义一个普遍可靠的"这两个 lambda 是否相同"规则。
5.5 SingleShotConnection 为什么只是连接标志
建立连接时,Qt 会把:
cpp
Qt::SingleShotConnection
从基础调度类型中拆出,记录到类似 isSingleShot 的字段。
发信号时,在真正调用或投递前移除连接,保证同一连接只触发一次。
先移除再调用很重要。否则槽内递归发出同一信号时,连接可能再次触发:
cpp
void Receiver::once()
{
emit sender->ready(); // 若连接尚未移除,会递归再次进入
}
5.6 为什么要同时挂到发送者和接收者
Qt 6 的 QObjectPrivate::addConnection() 概念上做两件事:
text
发送者侧:
按 signal index 添加到连接列表尾部
接收者侧:
添加到"谁连接到我"的 senders 反向链表
发送者侧的用途:
text
emit 时快速找到该信号的全部连接
接收者侧的用途:
text
receiver 析构时快速断开所有指向自己的连接
这解释了自动断连不是"运行时每次检查所有 QObject",而是连接建立时就维护了双向关系。
5.7 为什么直接连接通常按建立顺序调用
发送者侧每个信号维护连接列表,新增连接通常追加到尾部:
text
第一次 connect -> A
第二次 connect -> B
第三次 connect -> C
连接链表:A -> B -> C
发射时按链表遍历,因此同一个信号的直接连接通常按建立顺序调用。
但是不要把它扩展成错误结论:
- 不同线程的排队槽不保证和其他事件来源形成全局顺序。
- 槽内可以递归发信号。
- 槽内可以断开或新增连接。
- 不同发送者之间没有统一连接顺序。
如果业务正确性依赖 A 必须先于 B,应考虑让 A 明确调用 B,或建立状态机,而不是隐藏依赖在连接顺序中。
5.8 建立连接的完整概念流程
现代成员函数指针连接大致经历:
- 模板代码在编译期确认信号和槽参数兼容。
- 将信号成员函数映射为信号索引。
- 把槽成员函数或 functor 封装成统一可调用对象。
- 锁定发送者和接收者的信号槽内部数据。
- 处理
UniqueConnection和SingleShotConnection标志。 - 创建连接记录。
- 缓存接收对象的线程数据。
- 将记录挂入发送者按信号组织的连接列表。
- 在接收者侧记录反向关系,便于析构时清理。
- 返回
QMetaObject::Connection句柄。
连接记录概念上包含:
text
sender
signal index
receiver/context
slot method index 或 functor wrapper
connection type
queued parameter meta types
single-shot 等标志
5.9 QMetaObject::Connection 只是句柄
cpp
QMetaObject::Connection handle = connect(...);
句柄可以:
- 判断连接是否建立成功。
- 精确断开这一次连接。
- 交给
QScopedConnection类似的自定义 RAII 封装。
但句柄对象析构不会自动断开底层连接:
cpp
{
auto connection = connect(...);
} // connection 变量消失,底层连接仍存在
若希望离开作用域自动断开,需要自己实现或使用项目已有的 scoped connection 工具。
5.10 emit 到 QMetaObject::activate()
moc 生成的信号函数会调用内部激活逻辑,可以概括为:
#mermaid-svg-ST15aIuj2EWUOgky{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ST15aIuj2EWUOgky .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ST15aIuj2EWUOgky .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ST15aIuj2EWUOgky .error-icon{fill:#552222;}#mermaid-svg-ST15aIuj2EWUOgky .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ST15aIuj2EWUOgky .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ST15aIuj2EWUOgky .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ST15aIuj2EWUOgky .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ST15aIuj2EWUOgky .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ST15aIuj2EWUOgky .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ST15aIuj2EWUOgky .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ST15aIuj2EWUOgky .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ST15aIuj2EWUOgky .marker.cross{stroke:#333333;}#mermaid-svg-ST15aIuj2EWUOgky svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ST15aIuj2EWUOgky p{margin:0;}#mermaid-svg-ST15aIuj2EWUOgky .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ST15aIuj2EWUOgky .cluster-label text{fill:#333;}#mermaid-svg-ST15aIuj2EWUOgky .cluster-label span{color:#333;}#mermaid-svg-ST15aIuj2EWUOgky .cluster-label span p{background-color:transparent;}#mermaid-svg-ST15aIuj2EWUOgky .label text,#mermaid-svg-ST15aIuj2EWUOgky span{fill:#333;color:#333;}#mermaid-svg-ST15aIuj2EWUOgky .node rect,#mermaid-svg-ST15aIuj2EWUOgky .node circle,#mermaid-svg-ST15aIuj2EWUOgky .node ellipse,#mermaid-svg-ST15aIuj2EWUOgky .node polygon,#mermaid-svg-ST15aIuj2EWUOgky .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ST15aIuj2EWUOgky .rough-node .label text,#mermaid-svg-ST15aIuj2EWUOgky .node .label text,#mermaid-svg-ST15aIuj2EWUOgky .image-shape .label,#mermaid-svg-ST15aIuj2EWUOgky .icon-shape .label{text-anchor:middle;}#mermaid-svg-ST15aIuj2EWUOgky .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ST15aIuj2EWUOgky .rough-node .label,#mermaid-svg-ST15aIuj2EWUOgky .node .label,#mermaid-svg-ST15aIuj2EWUOgky .image-shape .label,#mermaid-svg-ST15aIuj2EWUOgky .icon-shape .label{text-align:center;}#mermaid-svg-ST15aIuj2EWUOgky .node.clickable{cursor:pointer;}#mermaid-svg-ST15aIuj2EWUOgky .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ST15aIuj2EWUOgky .arrowheadPath{fill:#333333;}#mermaid-svg-ST15aIuj2EWUOgky .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ST15aIuj2EWUOgky .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ST15aIuj2EWUOgky .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ST15aIuj2EWUOgky .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ST15aIuj2EWUOgky .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ST15aIuj2EWUOgky .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ST15aIuj2EWUOgky .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ST15aIuj2EWUOgky .cluster text{fill:#333;}#mermaid-svg-ST15aIuj2EWUOgky .cluster span{color:#333;}#mermaid-svg-ST15aIuj2EWUOgky div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ST15aIuj2EWUOgky .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ST15aIuj2EWUOgky rect.text{fill:none;stroke-width:0;}#mermaid-svg-ST15aIuj2EWUOgky .icon-shape,#mermaid-svg-ST15aIuj2EWUOgky .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ST15aIuj2EWUOgky .icon-shape p,#mermaid-svg-ST15aIuj2EWUOgky .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ST15aIuj2EWUOgky .icon-shape .label rect,#mermaid-svg-ST15aIuj2EWUOgky .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ST15aIuj2EWUOgky .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ST15aIuj2EWUOgky .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ST15aIuj2EWUOgky :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Direct
Queued
BlockingQueued
emit Sender::valueChanged(value)
进入 moc 生成的信号函数
QMetaObject 激活该 signal index
读取这个信号的连接记录
逐个连接判断类型
当前线程调用 slot/functor
复制参数,投递元调用事件
投递并等待完成
继续下一个连接
全部处理后从 emit 返回
概念信号函数:
cpp
void Sender::valueChanged(int value)
{
void *argv[] = {
nullptr,
&value
};
QMetaObject::activate(
this,
&staticMetaObject,
localSignalIndex,
argv);
}
这里传入:
text
sender:this
信号所属元对象:staticMetaObject
局部信号索引:localSignalIndex
参数地址数组:argv
QMetaObject::activate() 会把局部索引转换为继承链中的完整信号索引,再进入当前 Qt 6 源码中的 doActivate()。
5.11 doActivate() 第一件事:判断是否有必要遍历
源码会先检查:
- 发送者是否通过
blockSignals()阻塞信号。 - QML/声明式系统是否监听这个信号。
- 是否可能存在普通连接。
如果没有连接,可以很快返回。
这说明:
cpp
emit valueChanged(value);
在无人连接时不是完全零成本,但 Qt 会避免无意义地创建事件或遍历不存在的槽。
5.12 如何找到这个信号的连接列表
发送者内部维护类似:
text
SignalVector
├── signal 0 -> ConnectionList
├── signal 1 -> ConnectionList
├── signal 2 -> ConnectionList
└── all-signals/wildcard list
doActivate() 用 signal_index 定位列表,然后逐条读取连接。
它还记录发射开始时的最高连接 ID:
text
highestConnectionId
作用是避免在某个槽执行期间新建立的连接,突然参与当前这一轮发射。
例子:
cpp
connect(sender, &Sender::ready, receiverA, [&] {
connect(sender, &Sender::ready, receiverB, &ReceiverB::run);
});
emit sender->ready();
通常本轮只执行原先存在的 receiverA;新连接的 receiverB 从下一次发射开始参与。
这是一个很重要的"快照边界"思想。
5.13 AutoConnection 在发射时决定,不在 connect 时决定
connect() 建立时,发送者和接收者未来都可能 moveToThread()。所以 AutoConnection 不能在建立连接时永久决定 Direct 或 Queued。
doActivate() 在每次发射时比较:
text
当前执行 emit 的线程 ID
receiver 当前 thread data 中的线程 ID
逻辑近似:
cpp
if (type == Qt::AutoConnection && !receiverInSameThread)
queuedActivate();
else
directCall();
重点是当前发信线程,不是 sender->thread()。
例如:
cpp
sender->moveToThread(workerThread);
// 错误地在 GUI 线程直接调用 sender 的普通方法
sender->produce(); // produce 内部 emit ready()
这次 emit 发生在 GUI 线程。Auto 判断使用 GUI 当前线程,而不是看到 sender affinity 是 worker 就假装从 worker 发出。
5.14 Direct 分支如何调用槽
直接连接不创建事件。
Qt 根据连接目标类型选择统一调用方式:
text
lambda/functor
-> slotObj->call(receiver, argv)
moc 可快速调用的方法
-> static metacall function
一般元对象方法
-> QMetaObject::metacall(...)
无论哪条路径,最终都在当前发信线程、当前调用栈中执行真实槽代码。
调用栈近似:
text
业务函数
└── emit signal
└── moc 信号函数
└── QMetaObject::activate
└── doActivate
└── slot
所以 Direct 槽:
- 可以重入发送者。
- 可以让
emit很慢。 - 可以在持锁发信号时造成死锁。
- 抛出异常会穿过 Qt 内部分发栈,这是不受支持的设计。
5.15 Queued 分支:queued_activate()
跨线程 Auto 或显式 QueuedConnection 会进入排队分支。
Qt 6 源码中的核心工作:
- 取得信号参数的元类型列表。
- 确认所有参数都可以排队保存。
- 重新确认接收者仍存在。
- 创建
QQueuedMetaCallEvent。 - 使用元类型复制每个参数。
- 处理 single-shot。
- 再次确认连接没有在复制期间被断开。
QCoreApplication::postEvent(receiver, event)。
为什么要多次确认 receiver?
因为复制复杂参数可能耗时,期间其他线程可能断开连接或销毁接收对象。Qt 需要在锁和无锁区间之间重新验证状态。
5.16 QQueuedMetaCallEvent 如何复制参数
当前 Qt 6 源码会为排队事件保存:
text
参数指针数组
每个参数对应的 QMetaType
参数真实副本
槽调用信息
sender 和 signal id
概念代码:
cpp
for (int i = 1; i < argumentCount; ++i) {
types[i] = QMetaType(argumentMetaType[i]);
args[i] = types[i].create(originalArgs[i]);
}
小对象可能使用事件内部预分配空间,较大对象再单独分配。这属于性能优化细节,稳定结论是:
排队事件通过
QMetaType复制构造参数,并在事件析构时按类型销毁副本。
时间线:
text
发送线程栈上的 Result
↓ QMetaType copy construct
QQueuedMetaCallEvent 内的 Result 副本
↓ postEvent
接收线程事件队列
↓ 稍后执行
槽读取事件中的副本
↓ 事件析构
QMetaType 销毁副本
这就是为什么原始局部变量销毁后,排队槽仍能收到正常值。
5.17 排队事件怎样重新变成函数调用
目标线程事件循环取出事件后,最终进入:
cpp
receiver->event(event);
QObject::event() 对 QEvent::MetaCall 有专门分支,概念逻辑:
cpp
case QEvent::MetaCall:
metaCallEvent->placeMetaCall(this);
break;
placeMetaCall() 再根据连接目标执行:
text
slot object
-> 调用 lambda/functor/member callable
静态元调用函数
-> 调用 moc 生成分发入口
一般方法索引
-> QMetaObject::metacall
完整链路:
#mermaid-svg-qxh3M76ZpRjp8cuM{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qxh3M76ZpRjp8cuM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qxh3M76ZpRjp8cuM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qxh3M76ZpRjp8cuM .error-icon{fill:#552222;}#mermaid-svg-qxh3M76ZpRjp8cuM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qxh3M76ZpRjp8cuM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qxh3M76ZpRjp8cuM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qxh3M76ZpRjp8cuM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qxh3M76ZpRjp8cuM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qxh3M76ZpRjp8cuM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qxh3M76ZpRjp8cuM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qxh3M76ZpRjp8cuM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qxh3M76ZpRjp8cuM .marker.cross{stroke:#333333;}#mermaid-svg-qxh3M76ZpRjp8cuM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qxh3M76ZpRjp8cuM p{margin:0;}#mermaid-svg-qxh3M76ZpRjp8cuM .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qxh3M76ZpRjp8cuM .cluster-label text{fill:#333;}#mermaid-svg-qxh3M76ZpRjp8cuM .cluster-label span{color:#333;}#mermaid-svg-qxh3M76ZpRjp8cuM .cluster-label span p{background-color:transparent;}#mermaid-svg-qxh3M76ZpRjp8cuM .label text,#mermaid-svg-qxh3M76ZpRjp8cuM span{fill:#333;color:#333;}#mermaid-svg-qxh3M76ZpRjp8cuM .node rect,#mermaid-svg-qxh3M76ZpRjp8cuM .node circle,#mermaid-svg-qxh3M76ZpRjp8cuM .node ellipse,#mermaid-svg-qxh3M76ZpRjp8cuM .node polygon,#mermaid-svg-qxh3M76ZpRjp8cuM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qxh3M76ZpRjp8cuM .rough-node .label text,#mermaid-svg-qxh3M76ZpRjp8cuM .node .label text,#mermaid-svg-qxh3M76ZpRjp8cuM .image-shape .label,#mermaid-svg-qxh3M76ZpRjp8cuM .icon-shape .label{text-anchor:middle;}#mermaid-svg-qxh3M76ZpRjp8cuM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qxh3M76ZpRjp8cuM .rough-node .label,#mermaid-svg-qxh3M76ZpRjp8cuM .node .label,#mermaid-svg-qxh3M76ZpRjp8cuM .image-shape .label,#mermaid-svg-qxh3M76ZpRjp8cuM .icon-shape .label{text-align:center;}#mermaid-svg-qxh3M76ZpRjp8cuM .node.clickable{cursor:pointer;}#mermaid-svg-qxh3M76ZpRjp8cuM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qxh3M76ZpRjp8cuM .arrowheadPath{fill:#333333;}#mermaid-svg-qxh3M76ZpRjp8cuM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qxh3M76ZpRjp8cuM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qxh3M76ZpRjp8cuM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qxh3M76ZpRjp8cuM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qxh3M76ZpRjp8cuM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qxh3M76ZpRjp8cuM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qxh3M76ZpRjp8cuM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qxh3M76ZpRjp8cuM .cluster text{fill:#333;}#mermaid-svg-qxh3M76ZpRjp8cuM .cluster span{color:#333;}#mermaid-svg-qxh3M76ZpRjp8cuM div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qxh3M76ZpRjp8cuM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qxh3M76ZpRjp8cuM rect.text{fill:none;stroke-width:0;}#mermaid-svg-qxh3M76ZpRjp8cuM .icon-shape,#mermaid-svg-qxh3M76ZpRjp8cuM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qxh3M76ZpRjp8cuM .icon-shape p,#mermaid-svg-qxh3M76ZpRjp8cuM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qxh3M76ZpRjp8cuM .icon-shape .label rect,#mermaid-svg-qxh3M76ZpRjp8cuM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qxh3M76ZpRjp8cuM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qxh3M76ZpRjp8cuM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qxh3M76ZpRjp8cuM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 发送线程 emit
doActivate
QQueuedMetaCallEvent 复制参数
postEvent(receiver)
receiver 所属线程 posted-event 队列
事件循环 sendPostedEvents
sendEvent / notify
receiver->event(QEvent::MetaCall)
placeMetaCall
真实槽函数
5.18 BlockingQueued 如何实现等待
BlockingQueued 也会创建元调用事件并投递,但事件额外关联一个同步等待对象。当前 Qt 6 实现使用类似 latch 的同步原语。
发送线程:
text
创建计数为 1 的等待对象
创建 QMetaCallEvent 并携带等待对象
postEvent(receiver)
等待计数归零
接收线程:
text
取出 QMetaCallEvent
执行槽
事件处理完成,释放等待方
所以它不是"直接跨线程调用",而是:
text
排队调用 + 发送线程同步等待完成
同线程会死锁:
text
当前线程 post 给自己
当前线程开始等待
但只有当前线程事件循环能处理这个事件
事件循环永远得不到执行机会
Qt 源码会检测并警告这种情况,但不能替业务设计消除所有环形等待。
5.19 发射过程中断开连接会怎样
连接记录的 receiver 可以被置空,遍历时会跳过。
但是必须区分:
text
Direct:
尚未调用的连接通常可被当前遍历跳过
Queued:
如果 QMetaCallEvent 已经创建并成功 post,
disconnect 不一定能撤销队列中的事件
为什么?
因为排队后已经形成一个独立消息:
text
连接记录负责"是否继续产生新消息"
已经投递的事件负责"执行一次已经产生的消息"
这和退订邮件类似:
text
退订后不会再发送新邮件
已经进入收件箱的邮件不会自动消失
业务上要取消旧结果,应使用 request id、generation、取消令牌或状态校验。
5.20 发射过程中新增连接会怎样
前面提到 Qt 会记录发射开始时的最高 connection id。
这样:
cpp
void ReceiverA::onReady()
{
connect(sender, &Sender::ready,
receiverB, &ReceiverB::onReady);
}
新连接一般不会参与当前发射,避免遍历永远增长或行为不可预测。
下次:
cpp
emit sender->ready();
它才会进入正常连接列表。
5.21 发射过程中删除 sender 或 receiver
槽可以做非常激进的事情:
cpp
delete sender;
delete receiver;
Qt 内部因此不能简单保存裸指针后一路无检查地遍历。连接数据使用锁、原子状态、引用保护和延迟清理处理这些情况。
应用层仍不应故意依赖复杂自删除行为。更清晰的设计是:
- 使用
deleteLater()。 - 使用
QPointer检查对象是否仍存在。 - 返回当前调用栈后再做结构性销毁。
5.22 信号递归发射
Direct 槽内可以再次发出同一信号:
cpp
void Receiver::onValueChanged(int value)
{
if (value < 10)
emit sender->valueChanged(value + 1);
}
调用栈:
text
emit 1
└── slot(1)
└── emit 2
└── slot(2)
└── emit 3
风险:
- 深递归导致栈增长。
- 状态在外层槽尚未完成时再次变化。
- 连接顺序变得难以推理。
如果只是希望"稍后再处理",可用排队连接或 invokeMethod(..., QueuedConnection) 打断当前调用栈。
5.23 源码主线总结
text
connect
├── 编译期检查信号槽类型
├── 信号成员指针 -> signal index
├── 槽/lambda -> slot object 或方法索引
├── 创建 Connection
├── 加入 sender 的按信号连接列表
└── 加入 receiver 的反向连接列表
emit
├── moc 信号函数
├── QMetaObject::activate
├── doActivate 遍历连接
├── Direct -> 当前栈调用
├── Queued -> QQueuedMetaCallEvent + postEvent
└── BlockingQueued -> postEvent + 等待完成
接收线程
├── 事件循环取出 MetaCall 事件
├── notify / event filter
├── QObject::event(QEvent::MetaCall)
├── placeMetaCall
└── 真实槽函数
直接连接的多个槽通常按连接建立顺序调用。
Qt 在遍历时还要处理:
- 某个槽执行期间接收者被销毁。
- 槽内新增或断开连接。
- 递归再次发出同一信号。
- 一次性连接。
- 多线程同时连接或断开。
这也是信号分发比一次普通函数调用多出开销的原因。
5.24 断开连接后的排队事件
一个容易忽略的细节:
断开排队连接后,已经投递到事件队列的元调用事件仍可能被执行。
因此,disconnect() 不是"撤销所有已经发生的异步投递"。
如果业务上必须丢弃旧结果,可以增加:
- generation/version 编号。
- request id。
- 取消标志。
- 状态检查。
- 将接收 context 销毁,让后续目标失效。
示例:
cpp
void Controller::startRequest()
{
const quint64 requestId = ++m_currentRequestId;
connect(worker, &Worker::resultReady, this,
[this, requestId](Result result) {
if (requestId != m_currentRequestId)
return;
applyResult(std::move(result));
},
Qt::QueuedConnection | Qt::SingleShotConnection);
}
6. 事件是什么
QEvent 表示一次需要对象处理的事件。来源包括:
- 操作系统窗口系统:鼠标、键盘、窗口大小变化。
- Qt 内部:绘制、布局、焦点、延迟删除。
- 定时器。
- socket notifier 和异步 I/O 通知。
postEvent()手工投递。- 排队信号槽生成的内部元调用事件。
- 用户自定义事件。
事件和信号不是同一个抽象:
- 事件通常有一个明确目标对象,并沿事件分发路径处理。
- 信号描述对象状态变化,可以零个、一到多个接收者。
- 某个事件处理函数可以发信号,例如按钮处理鼠标释放后发出
clicked()。 - 排队信号槽内部又借助事件队列完成延迟调用。
7. 事件循环如何运行
7.1 主事件循环
典型程序:
cpp
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow window;
window.show();
return app.exec();
}
app.exec() 进入主线程事件循环。概念过程:
cpp
while (!quitRequested) {
processReadyPostedNativeTimerAndIoEvents();
if (nothingIsReady())
waitUntilSomethingWakesTheThread();
}
这个伪代码故意没有写死具体先后顺序,因为真正的平台等待和分发由 QAbstractEventDispatcher 的后端实现。
7.2 QCoreApplication::exec() 并不直接处理每一种事件
调用链可以简化为:
text
QCoreApplication::exec()
↓
创建/使用 QEventLoop
↓
QEventLoop::exec()
↓
while (!exit)
eventDispatcher->processEvents(...)
Qt 6 QEventLoop::exec() 的核心思想近似:
cpp
while (!exitRequested)
processEvents(flags | WaitForMoreEvents | EventLoopExec);
而 QEventLoop::processEvents() 本身又只是把工作交给当前线程的事件分发器:
cpp
return threadData->eventDispatcher->processEvents(flags);
因此职责分层是:
text
QCoreApplication
管理应用级生命周期和主循环入口
QEventLoop
管理某一层循环的进入、退出和 loop level
QAbstractEventDispatcher
与平台等待机制对接,真正收集和激活事件源
7.3 为什么需要平台事件分发器
不同操作系统提供不同事件来源:
Windows:
- 窗口消息队列。
WM_TIMER。- socket notifier 对应的内部窗口消息。
- Qt 自己的唤醒消息。
Unix/Linux:
- 文件描述符 readiness。
poll/相关平台后端。- 定时器的最近截止时间。
- Qt 内部唤醒管道或事件描述符。
macOS/iOS:
- 原生 run loop 和平台事件源。
Qt 不可能用一段完全相同的 C++ while 代码高效处理所有平台,所以抽象出 QAbstractEventDispatcher。
应用代码通常不直接创建它。Qt 会:
- 创建
QCoreApplication时为主线程准备事件分发器。 - 启动辅助
QThread时为该线程准备事件分发器。
7.4 事件循环有哪些输入源
一个线程的事件循环通常整合:
Qt posted events
来源:
cpp
QCoreApplication::postEvent(...)
QObject::deleteLater()
QueuedConnection
QMetaObject::invokeMethod(..., Qt::QueuedConnection)
它们最终进入该线程的 posted-event 列表。
平台窗口系统事件
例如:
- 鼠标。
- 键盘。
- 触摸。
- 窗口显示、移动、缩放。
- 原生关闭消息。
平台插件把原生事件转换为 Qt 事件或更高层输入分发。
定时器
QTimer 注册截止时间。事件分发器计算最近到期时间,用它决定最多等待多久。
socket notifier / I/O readiness
Unix 下可把 socket/file descriptor 放入等待集合。准备好时,激活对应 notifier。
跨线程唤醒
其他线程调用:
cpp
QCoreApplication::postEvent(receiver, event);
不仅要把事件放入目标线程队列,还要调用目标事件分发器的 wakeUp(),否则目标线程可能正无限睡眠。
7.5 postEvent() 为什么可以唤醒正在睡眠的线程
Qt 6 源码概念过程:
text
1. 根据 receiver->thread() 找到目标 QThreadData
2. 锁住目标线程的 postEventList
3. 插入 QPostEvent
4. 标记目标线程现在不能继续睡
5. 解锁
6. 调用目标 eventDispatcher->wakeUp()
Unix 后端通常让内部唤醒管道/描述符变为可读,使 poll 返回。
Windows 后端通常通过内部消息/等待机制让消息循环醒来。
所以跨线程排队不是不断轮询:
text
目标线程休眠
↓
发送线程 postEvent
↓
wakeUp 触发平台等待源
↓
目标线程从等待返回
↓
下一轮处理 posted event
7.6 一轮事件循环的通用阶段
跨平台可以把一轮理解为:
- 标记事件分发器处于唤醒状态。
- 分发当前允许处理的 posted events。
- 根据 flags 决定是否包括用户输入、socket notifier、timer。
- 判断当前是否还有立即可做的工作。
- 如果没有并允许等待,发出
aboutToBlock()。 - 进入平台等待。
- 因系统消息、I/O、timer 到期或
wakeUp()返回。 - 发出/进入 awake 状态。
- 激活准备好的平台事件、socket notifier 和 timer。
- 返回上层
QEventLoop,如果未退出则进入下一轮。
这只是通用模型。第 2、9 步的内部先后和粒度因后端而异。
7.7 Unix 后端的一轮顺序示例
当前 Qt 6 通用 Unix 事件分发器的核心顺序可以概括为:
text
1. emit awake
2. sendPostedEvents
3. 计算是否允许处理 timer/socket
4. 计算是否可以阻塞等待
5. 必要时 emit aboutToBlock
6. poll(socket fds + wake-up pipe, 最近 timer deadline)
7. 检查 wake-up pipe
8. 激活 socket notifiers
9. 激活 timers
10. 返回本轮是否处理了事件
这里能观察到当前实现中:
- posted events 在进入
poll前处理。 - poll 返回后,socket notifier 在 timer 激活之前。
但不要把这个实现顺序写成跨平台业务协议。Qt 未来版本或使用 glib 等其他后端时,细节可以不同。
7.8 Windows 后端的一轮顺序示例
当前 Qt 6 Win32 后端的核心顺序大致是:
text
1. emit awake
2. 每轮先发送一次 posted events
3. 处理之前因 flags 暂存的用户输入/socket 消息
4. 从 Windows 消息队列取 MSG
5. 过滤或暂存不允许处理的消息
6. 处理 Qt 内部唤醒、timer、quit 等消息
7. TranslateMessage / DispatchMessage
8. 无工作且允许等待时 emit aboutToBlock
9. 等待新 Windows 消息
10. 醒来后继续
Windows 源码中特意限制 posted events 每轮发送一次,以避免某些持续自投递场景造成活锁。
7.9 所以"Qt 事件处理顺序"正确答案是什么
可以分层回答。
Qt 明确保证的顺序
对于 QCoreApplication::postEvent() 的队列:
- 按 priority 从高到低。
- 相同 priority 按投递顺序。
对于单个对象的过滤分发:
- 应用级过滤器先于对象级过滤器。
- 对象级过滤器中后安装的先执行。
- 过滤器都返回
false后才进入receiver->event()。
对于同一信号的直接连接:
- 通常按连接建立顺序依次调用。
不应依赖的全局顺序
不要假设:
- queued signal 一定早于 0ms timer。
- timer 一定早于 socket notifier。
- 鼠标事件一定早于 posted custom event。
- 两个不同线程发来的 queued signal 能形成全局总顺序。
- 在一个槽中 post 的事件一定会在当前
sendPostedEvents()批次立即执行。
这些顺序可能受:
- 平台后端。
- 事件优先级。
- 投递发生时间。
- 是否处于当前批次边界。
- 嵌套事件循环。
- flags。
- 原生消息队列状态。
影响。
7.10 posted event 队列内部如何排序
目标线程中有一个 posted-event 列表。每个元素概念上是:
cpp
struct QPostEventConcept
{
QObject *receiver;
QEvent *event;
int priority;
};
插入规则:
text
高 priority 在前
同 priority 使用 upper_bound 插入到同优先级已有事件之后
例如投递:
text
A priority 0
B priority 10
C priority 0
D priority -5
队列顺序:
text
B(10) -> A(0) -> C(0) -> D(-5)
A 和 C 相同优先级,保持 FIFO。
7.11 为什么本轮处理期间新 post 的事件通常留到下一轮
sendPostedEvents() 开始时会记录一个批次边界,概念上类似:
cpp
const auto endOfCurrentBatch = queue.size();
遍历到这个边界就停止。处理事件期间新投递的事件留在队列中,由下一轮处理。
目的:
- 防止某个事件不断 post 自己,导致当前循环永远出不去。
- 给 timer、socket 和系统输入获得处理机会。
- 控制重入和队列结构变化。
例子:
cpp
void Receiver::customEvent(QEvent *)
{
QCoreApplication::postEvent(this, new MyEvent);
}
如果新事件必须在同一批次继续执行:
text
处理一个 -> 再投一个 -> 立即处理 -> 再投一个
sendPostedEvents() 可能永远无法返回。批次边界避免这种 live-lock。
7.12 sendPostedEvents() 如何真正投递
Qt 6 源码中的核心步骤:
- 锁定当前线程的 posted-event 列表。
- 记录递归深度和当前批次插入边界。
- 找到符合 receiver/type 条件的事件。
- 对
DeferredDelete做特殊层级判断。 - 从列表中逻辑移除事件。
- 解锁队列。
- 调用
QCoreApplication::sendEvent(receiver, event)。 sendEvent()进入notify()分发链。- 事件处理后删除事件对象。
- 重新加锁继续下一个。
为什么调用用户事件处理函数前要解锁?
因为用户代码可以:
- 再次
postEvent()。 - 删除对象。
- 进入嵌套循环。
- 安装/移除过滤器。
如果持有队列锁调用未知用户代码,很容易死锁。
7.13 每个线程可以有自己的事件循环
默认 QThread::run() 会调用 exec(),因此常规 QThread 可以运行线程局部事件循环。
事件队列不是一个全局队列:
- 每个有事件分发器的线程处理自己的对象。
QObject::thread()表示该对象的线程亲和性。- 投递给对象的事件由对象所属线程取出。
QThread::exec() 内部会创建一个局部 QEventLoop 并调用其 exec()。
所以:
text
QThread::start()
↓ 新操作系统线程
QThread::run()
↓ 默认实现
QThread::exec()
↓
QEventLoop::exec()
↓
当前线程 event dispatcher
7.14 事件循环为什么属于线程,而不是对象
一个线程可以拥有很多 QObject:
text
GUI thread
├── MainWindow
├── QPushButton
├── QTimer
└── Controller
它们共享该线程的事件循环。
事件中保存 receiver:
text
线程队列取出事件
↓
看 receiver 是谁
↓
投递给对应对象
所以不是每个对象一个循环,而是:
text
每个线程一个事件分发上下文
多个 QObject 共享
每个事件指定目标 QObject
7.15 为什么阻塞 GUI 线程会卡界面
cpp
void MainWindow::onStartClicked()
{
expensiveCalculation(); // 运行 10 秒
}
这 10 秒内主线程无法返回事件循环,因此:
- 窗口不能重绘。
- 鼠标键盘事件不能处理。
- 主线程定时器不触发。
- 排队到主线程的槽不执行。
deleteLater()延后。- 操作系统可能标记程序"未响应"。
正确方向是:
- 把长计算移到工作线程。
- 将任务拆成短步骤,由定时器或状态机驱动。
- 真正异步使用网络、进程和 I/O API。
不要把频繁调用 processEvents() 当成常规修复。
7.16 "槽很短"为什么是事件驱动程序的重要规则
事件循环是串行执行当前线程中的回调:
text
事件 A -> 处理完
事件 B -> 处理完
事件 C -> 处理完
如果事件 B 的槽运行 10 秒:
text
事件 A -> 完成
事件 B -> 运行 10 秒
事件 C -> 等 10 秒
timer -> 等
paint -> 等
queued slot -> 等
不是 Qt 不会多任务,而是同一个线程同一时刻只能执行一个调用栈。
可以把事件循环想成单窗口办事柜台:
text
每个任务本身不一定多
但某个人占着窗口 10 秒
后面所有任务都排队
7.17 0ms timer 是否"立即执行"
cpp
QTimer::singleShot(0, receiver, [] {
// ...
});
它表达的是:
text
当前调用栈返回事件循环后,尽快安排执行
不是:
text
在当前语句之后绝对立刻执行
它和其他 ready 事件的相对顺序不应作为业务协议。若必须严格排序,应在同一函数中明确调用,或使用单一串行队列和序号。
8. 事件的完整分发路径
对普通 QObject 可以用下面的模型理解:
#mermaid-svg-mMG7FNt5fIpR9DWY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mMG7FNt5fIpR9DWY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mMG7FNt5fIpR9DWY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mMG7FNt5fIpR9DWY .error-icon{fill:#552222;}#mermaid-svg-mMG7FNt5fIpR9DWY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mMG7FNt5fIpR9DWY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mMG7FNt5fIpR9DWY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mMG7FNt5fIpR9DWY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mMG7FNt5fIpR9DWY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mMG7FNt5fIpR9DWY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mMG7FNt5fIpR9DWY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mMG7FNt5fIpR9DWY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mMG7FNt5fIpR9DWY .marker.cross{stroke:#333333;}#mermaid-svg-mMG7FNt5fIpR9DWY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mMG7FNt5fIpR9DWY p{margin:0;}#mermaid-svg-mMG7FNt5fIpR9DWY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mMG7FNt5fIpR9DWY .cluster-label text{fill:#333;}#mermaid-svg-mMG7FNt5fIpR9DWY .cluster-label span{color:#333;}#mermaid-svg-mMG7FNt5fIpR9DWY .cluster-label span p{background-color:transparent;}#mermaid-svg-mMG7FNt5fIpR9DWY .label text,#mermaid-svg-mMG7FNt5fIpR9DWY span{fill:#333;color:#333;}#mermaid-svg-mMG7FNt5fIpR9DWY .node rect,#mermaid-svg-mMG7FNt5fIpR9DWY .node circle,#mermaid-svg-mMG7FNt5fIpR9DWY .node ellipse,#mermaid-svg-mMG7FNt5fIpR9DWY .node polygon,#mermaid-svg-mMG7FNt5fIpR9DWY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mMG7FNt5fIpR9DWY .rough-node .label text,#mermaid-svg-mMG7FNt5fIpR9DWY .node .label text,#mermaid-svg-mMG7FNt5fIpR9DWY .image-shape .label,#mermaid-svg-mMG7FNt5fIpR9DWY .icon-shape .label{text-anchor:middle;}#mermaid-svg-mMG7FNt5fIpR9DWY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mMG7FNt5fIpR9DWY .rough-node .label,#mermaid-svg-mMG7FNt5fIpR9DWY .node .label,#mermaid-svg-mMG7FNt5fIpR9DWY .image-shape .label,#mermaid-svg-mMG7FNt5fIpR9DWY .icon-shape .label{text-align:center;}#mermaid-svg-mMG7FNt5fIpR9DWY .node.clickable{cursor:pointer;}#mermaid-svg-mMG7FNt5fIpR9DWY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mMG7FNt5fIpR9DWY .arrowheadPath{fill:#333333;}#mermaid-svg-mMG7FNt5fIpR9DWY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mMG7FNt5fIpR9DWY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mMG7FNt5fIpR9DWY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mMG7FNt5fIpR9DWY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mMG7FNt5fIpR9DWY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mMG7FNt5fIpR9DWY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mMG7FNt5fIpR9DWY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mMG7FNt5fIpR9DWY .cluster text{fill:#333;}#mermaid-svg-mMG7FNt5fIpR9DWY .cluster span{color:#333;}#mermaid-svg-mMG7FNt5fIpR9DWY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mMG7FNt5fIpR9DWY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mMG7FNt5fIpR9DWY rect.text{fill:none;stroke-width:0;}#mermaid-svg-mMG7FNt5fIpR9DWY .icon-shape,#mermaid-svg-mMG7FNt5fIpR9DWY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mMG7FNt5fIpR9DWY .icon-shape p,#mermaid-svg-mMG7FNt5fIpR9DWY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mMG7FNt5fIpR9DWY .icon-shape .label rect,#mermaid-svg-mMG7FNt5fIpR9DWY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mMG7FNt5fIpR9DWY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mMG7FNt5fIpR9DWY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mMG7FNt5fIpR9DWY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 未消费
未消费
return true
return true
平台事件或 posted event
QCoreApplication::notify
应用级事件过滤器
对象级事件过滤器,后安装的先执行
receiver->event(event)
mousePressEvent / keyPressEvent / timerEvent / customEvent 等
停止分发
8.1 sendEvent() 和事件循环最终汇合到同一分发链
同步路径:
text
QCoreApplication::sendEvent(receiver, event)
↓
notifyInternal2
↓
QCoreApplication::notify / notify_helper
↓
event filters
↓
receiver->event(event)
异步 posted event:
text
postEvent
↓
目标线程队列
↓
event dispatcher
↓
sendPostedEvents
↓
sendEvent
↓
同一 notify / filter / event 链
也就是说:
postEvent()和排队信号槽只是把"何时进入分发链"推迟了;真正交给对象时,仍走统一事件通知路径。
8.2 应用级过滤器为什么最先
Qt 6 notify_helper() 的核心顺序概念上是:
cpp
if (applicationEventFilterConsumes(receiver, event))
return true;
if (objectEventFilterConsumes(receiver, event))
return true;
return receiver->event(event);
应用级过滤器用于全局观察,所以在目标对象自己的过滤器之前。
代价也很明显:
text
主线程中的大量事件
都要先经过全局过滤器
如果全局过滤器进行日志 I/O、复杂查找或锁等待,会拖慢整个应用事件分发。
8.3 对象级过滤器为什么后安装先调用
installEventFilter() 会把过滤器放到列表前部。假设:
cpp
target->installEventFilter(filterA);
target->installEventFilter(filterB);
target->installEventFilter(filterC);
内部顺序近似:
text
C -> B -> A
事件到来:
text
filterC
false
filterB
true
停止
那么:
filterA看不到事件。target->event()也看不到事件。
8.4 event() 为什么还要再调用专用函数
以 QObject::event() 为例,内部本质上是一个类型分派器:
cpp
switch (event->type()) {
case QEvent::Timer:
timerEvent(static_cast<QTimerEvent *>(event));
break;
case QEvent::ChildAdded:
case QEvent::ChildRemoved:
childEvent(static_cast<QChildEvent *>(event));
break;
case QEvent::DeferredDelete:
delete this;
break;
case QEvent::MetaCall:
metaCallEvent->placeMetaCall(this);
break;
}
QWidget::event() 在此基础上继续分发:
text
QEvent::MouseButtonPress -> mousePressEvent()
QEvent::KeyPress -> keyPressEvent()
QEvent::Paint -> paintEvent()
QEvent::Resize -> resizeEvent()
所以专用函数不是绕开 event(),而是由 event() 根据类型调用。
8.5 重写 event() 后为什么要调用基类
错误:
cpp
bool MyWidget::event(QEvent *event)
{
if (event->type() == MyEventType)
return true;
return false;
}
这会让基类无法处理:
- paint。
- focus。
- key。
- layout。
- timer。
- meta call。
- deferred delete。
正确:
cpp
bool MyWidget::event(QEvent *event)
{
if (event->type() == MyEventType) {
handleMyEvent(event);
return true;
}
return QWidget::event(event);
}
8.6 排队信号槽为什么也能被事件过滤器看到
QueuedConnection 生成的是 QEvent::MetaCall 类型的内部事件。
因此对象事件过滤器可以观察到它经过事件分发系统。不过不要依赖私有事件内容去修改信号槽调用:
- 内部事件类不是公共业务协议。
- 过滤掉 MetaCall 会阻止排队槽执行,影响面很大。
- Qt 内部实现可能变化。
它更适合作为理解和诊断线索,而不是业务扩展点。
8.7 spontaneous() 表示什么
QEvent::spontaneous() 用于区分事件是否源自应用外部/平台系统。
粗略理解:
text
平台窗口系统产生的输入事件 -> spontaneous 通常为 true
sendEvent/postEvent 人工发送 -> spontaneous 为 false
它不表示:
- 事件是否同步。
- 事件是否跨线程。
- 事件优先级。
- 用户一定亲自触发。
8.8 事件"被处理"和"被接受"是两层含义
后文会详细讲 event() 返回值与 accept()。这里先建立分层:
text
过滤器 return true
表示阻止后续分发
event() return true
表示对象识别/处理了此事件
event->accept()
表示该事件类型定义下的接受状态
三者不能简单视为同一个布尔值。
不同模块还可能有额外传播规则,例如:
- 某些键盘和鼠标事件在未接受时向父 widget 传播。
- Qt Quick 有自己的 item 事件和指针处理体系。
- Graphics View 有 scene event 和 scene event filter。
不要把某一种 widget 的冒泡行为误认为所有 QEvent 都会自动向父对象传播。
8.9 传播与过滤不是同一件事
过滤发生在事件交给当前 receiver 之前。
传播通常是某些事件体系在当前 receiver 不接受后,再尝试父 widget 或其他目标。
例如键盘事件可能出现:
text
焦点 widget 的过滤器
↓
焦点 widget event/keyPressEvent
↓ 未接受
父 widget 的事件处理
父 widget 收到时,又会走它自己的过滤和 event() 路径。
但普通自定义 QEvent 不会因为你返回 false 就自动遍历 QObject parent 树。传播规则由具体事件和模块定义。
9. 五种常见事件处理层级
从局部到全局大致有:
9.1 重写专用处理函数
cpp
void Editor::keyPressEvent(QKeyEvent *event)
{
if (event->key() == Qt::Key_Escape) {
cancelEditing();
event->accept();
return;
}
QWidget::keyPressEvent(event);
}
这是 widget 事件处理的首选,语义最清晰。
9.2 重写 QObject::event()
cpp
bool Editor::event(QEvent *event)
{
if (event->type() == QEvent::ToolTip) {
showCustomToolTip();
return true;
}
return QWidget::event(event);
}
适合:
- 需要统一处理多个事件类型。
- 需要处理没有专用虚函数的事件。
- 需要在专用处理函数之前介入。
未处理事件应调用基类实现,否则可能破坏焦点、快捷键、绘制等默认行为。
9.3 对象级事件过滤器
cpp
class ShortcutFilter : public QObject
{
Q_OBJECT
protected:
bool eventFilter(QObject *watched, QEvent *event) override
{
if (event->type() == QEvent::KeyPress) {
auto *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_F1) {
showHelpFor(watched);
return true;
}
}
return QObject::eventFilter(watched, event);
}
};
auto *filter = new ShortcutFilter(parent);
editor->installEventFilter(filter);
适合:
- 无法修改被观察类。
- 一个控制器统一观察多个对象。
- 临时拦截交互。
9.4 应用级事件过滤器
cpp
qApp->installEventFilter(globalFilter);
可以观察主线程中应用对象的广泛事件。代价是:
- 每个事件都要经过过滤器,容易产生性能问题。
- 逻辑过于全局,难以维护。
- 误消费事件会影响整个应用。
仅用于确实全局的需求,例如空闲检测、统一快捷键策略或调试诊断。
9.5 重写 QCoreApplication::notify()
这是更底层的全局入口,能力强但耦合也强。一般业务代码不应依赖它处理普通交互。Qt 文档还提示 Qt 7 对非主线程对象的 notify() 行为将变化,因此新设计应避免用它监控所有线程事件。
10. 事件过滤器的关键规则
10.1 返回值
cpp
return true; // 事件已被过滤/消费,不再交给 watched
return false; // 继续后续过滤器和 watched->event()
如果只是观察日志,通常返回 false。
10.2 调用顺序
一个对象安装多个过滤器时:
- 后安装的过滤器先执行。
- 某个过滤器返回
true后,后续分发停止。
因此动态安装和移除过滤器时,应避免依赖隐晦顺序完成核心业务。
10.3 线程要求
过滤器对象和被观察对象必须生活在同一线程,否则过滤器不会按预期工作。
这和事件处理的基本规则一致:不要让一个线程直接执行另一个线程对象的事件逻辑。
10.4 在过滤器中删除对象
如果过滤器删除了被观察对象,必须返回 true,不能让 Qt 接着把当前事件发送给已删除对象:
cpp
bool Filter::eventFilter(QObject *watched, QEvent *event)
{
if (shouldDestroy(watched, event)) {
delete watched;
return true;
}
return false;
}
跨复杂调用栈时通常优先 deleteLater(),但仍要明确当前事件是否应继续。
10.5 移除过滤器
cpp
watched->removeEventFilter(filter);
过滤器对象销毁时相关关系会清理,但显式移除能更清楚表达临时拦截的结束时机。
11. sendEvent() 与 postEvent()
11.1 sendEvent()
同步发送:
cpp
QEvent event(MyEventType);
const bool handled = QCoreApplication::sendEvent(receiver, &event);
特点:
- 当前调用栈立即进入事件分发。
- 返回事件处理结果。
- Qt 不接管事件对象,可放在栈上。
- 只能安全地发送给当前线程中的对象。
- 不要求先返回事件循环。
它会产生重入:被调用对象可能反过来调用当前对象。
11.2 postEvent()
异步投递:
cpp
QCoreApplication::postEvent(
receiver,
new MyEvent(payload),
Qt::NormalEventPriority);
特点:
- 立即返回。
- 事件进入 receiver 所属线程队列。
- 事件必须在堆上创建。
- Qt 接管所有权,投递后不能再访问该指针。
- 可从其他线程调用。
- 目标线程必须处理事件。
- 可以指定优先级。
某些事件会被压缩,例如重复的绘制或尺寸变化请求可能合并,以减少无意义的重复工作。
postEvent() 的所有权转移发生在哪里
调用:
cpp
QEvent *event = new MyEvent;
QCoreApplication::postEvent(receiver, event);
成功进入 postEvent() 后,事件所有权交给 Qt:
cpp
// 不要再做这些事
delete event;
event->setAccepted(false);
qDebug() << event->type();
原因是:
- 事件可能已被压缩并删除。
- 可能已被其他线程取走。
- 稍后处理后 Qt 会负责销毁。
事件优先级
cpp
QCoreApplication::postEvent(
receiver,
new MyEvent,
Qt::HighEventPriority);
优先级影响同一线程 posted-event 队列内部顺序:
text
高值优先
同值 FIFO
不要把高优先级当实时调度:
- 它不能抢占当前正在执行的槽。
- 它不能让阻塞中的线程立刻执行事件。
- 它不定义相对系统消息、timer 和 socket 的跨平台总顺序。
为什么事件会被压缩
假设窗口连续收到 100 次 update():
text
每次都立刻绘制
-> 重复工作
-> 闪烁
-> 浪费 CPU
更合理:
text
合并为一次待绘制请求
-> 当前批次结束后画最新状态
Qt 会对部分可压缩事件做合并。具体哪些事件、在哪一层压缩与类型和模块有关。
重要结论:
postEvent()不等于"每调用一次,接收者一定收到一次完全独立事件"。
如果业务要求每条消息都不能丢,不应借用可能被框架压缩的事件类型。
事件压缩和业务合并的区别
框架事件压缩解决 UI/系统层重复工作。
业务背压仍要自己设计:
text
摄像头帧
日志记录
金融成交
网络数据包
不能假设 Qt 会自动为这些业务消息选择正确的丢弃策略。
11.3 对比
sendEvent() |
postEvent() |
|
|---|---|---|
| 时机 | 立即 | 稍后 |
| 调用线程 | 当前线程 | 目标对象线程处理 |
| 事件所有权 | 调用者 | 投递后交给 Qt |
| 是否需要事件循环 | 否 | 是 |
| 是否容易重入 | 是 | 降低当前栈重入,但未来仍可能重入 |
| 跨线程 | 不应用于跨线程直接分发 | 支持线程安全投递 |
12. 自定义事件
定义唯一事件类型:
cpp
class DataEvent : public QEvent
{
public:
static QEvent::Type typeId()
{
static const int type = QEvent::registerEventType();
return static_cast<QEvent::Type>(type);
}
explicit DataEvent(QByteArray data)
: QEvent(typeId()),
m_data(std::move(data))
{
}
const QByteArray &data() const
{
return m_data;
}
private:
QByteArray m_data;
};
接收:
cpp
void Receiver::customEvent(QEvent *event)
{
if (event->type() == DataEvent::typeId()) {
auto *dataEvent = static_cast<DataEvent *>(event);
consume(dataEvent->data());
return;
}
QObject::customEvent(event);
}
发送:
cpp
QCoreApplication::postEvent(
receiver,
new DataEvent(payload));
自定义事件还是排队信号?
优先排队信号的情况:
- 发送者不应知道具体接收者。
- 希望一对多。
- 参数可以自然表达消息。
- 希望自动断连。
优先自定义事件的情况:
- 消息天然面向一个具体对象。
- 需要事件优先级。
- 需要统一经过事件过滤器。
- 正在实现事件驱动框架或特殊分发协议。
13. event()->accept() 和返回值不要混淆
有两个不同概念:
event() 的返回值
表示接收对象是否识别/处理了该事件,用于分发流程。
QEvent::accept() / ignore()
是事件对象内部的接受状态。某些事件体系根据它决定是否继续传播,例如键盘、鼠标、关闭和拖放事件。
下面两段不总是完全等价:
cpp
event->accept();
return true;
cpp
event->ignore();
return true;
具体传播语义要查对应事件类和控件体系文档。
14. 嵌套事件循环和重入
下面的 API 可能启动局部事件循环:
QDialog::exec()QEventLoop::exec()- 某些同步等待封装
嵌套循环期间,外层函数虽然没有返回,但其他事件和排队槽可能继续运行:
cpp
void Controller::edit()
{
m_editing = true;
dialog.exec(); // 这里可能处理大量其他事件
// 到这里时,其他槽可能已经改变甚至销毁了相关对象。
m_editing = false;
}
风险:
- 状态被意外重入。
- 同一动作执行两次。
- 对象在嵌套循环中被
deleteLater()或间接销毁。 - 调用栈上的假设失效。
工程建议:
- 优先
open()+ finished 信号的异步对话框模式。 - 用状态机明确禁止重复进入。
- 临界对象使用
QPointer检查存活。 - 不在持锁期间进入可能处理事件的 API。
14.1 loop level 是什么
线程进入主循环:
text
loop level = 1
在某个槽中调用:
cpp
dialog.exec();
又进入一个局部循环:
text
loop level = 2
关闭对话框后,内层循环退出:
text
loop level 回到 1
Qt 内部记录循环层级,原因之一就是正确处理延迟删除,避免对象在错误的嵌套层级过早销毁。
14.2 一个具体重入例子
cpp
void Controller::save()
{
if (m_saving)
return;
m_saving = true;
QMessageBox::information(
nullptr,
QStringLiteral("Saving"),
QStringLiteral("Please wait"));
writeFile();
m_saving = false;
}
QMessageBox 的阻塞式调用可能运行嵌套事件循环。期间:
- 用户可能触发其他动作。
- queued slot 可能执行。
- timer 可能触发。
- 另一个路径可能再次调用
save()。 - 当前对象可能被关闭。
m_saving 防止了一部分重复进入,但无法保证所有成员仍有效。
更稳妥的异步设计:
cpp
auto *box = new QMessageBox(..., this);
connect(box, &QMessageBox::finished, this, [this, box] {
box->deleteLater();
writeFile();
});
box->open();
调用栈返回主事件循环,状态转换更显式。
15. 为什么不推荐滥用 processEvents()
典型反模式:
cpp
for (int i = 0; i < 1'000'000; ++i) {
doOneStep(i);
QCoreApplication::processEvents();
}
看似"界面不卡",实际引入:
- 任意槽和输入事件在循环中间重入。
- 当前对象可能被关闭或删除。
- 循环依赖的数据可能被其他回调修改。
DeferredDelete等事件在某些局部循环写法中处理异常。- 性能和响应时序难以预测。
更好的方案:
- 工作线程执行重计算。
- 每次只处理一批,通过零间隔定时器继续下一批。
- 使用异步 API 和状态机。
- 只在受控、短暂且充分理解重入风险的工具代码中使用
processEvents()。
15.1 它不是"刷新一下界面"这么简单
调用:
cpp
QCoreApplication::processEvents();
可能执行:
- 鼠标键盘事件。
- queued slot。
- timer。
- socket notifier。
- 窗口关闭。
- 对象销毁相关流程。
所以前后状态可能完全不同:
cpp
QPointer<MyDialog> guard = dialog;
QCoreApplication::processEvents();
if (!guard)
return; // 对话框可能已在事件处理中被删除
15.2 不同重载的批次行为
Qt 6 文档区分:
- 无 deadline 的
processEvents(flags)主要处理调用前已经可用的事件;执行期间新 post 的事件通常留到后续轮次。 - 带 deadline 的重载会在时间范围内继续处理执行期间新进入的事件。
因此下面两种代码的重入和饥饿风险不同:
cpp
QCoreApplication::processEvents(flags);
cpp
QCoreApplication::processEvents(flags, deadline);
不要只看函数名相同就认为行为完全等价。
15.3 为什么局部循环可能影响 DeferredDelete
deleteLater() 依赖事件循环层级和专门的 DeferredDelete 处理。
如果代码只是不断手工调用某种 processEvents(),却没有正常返回对应事件循环,延迟删除可能不会按预期发生。
这会造成:
- 看似内存泄漏。
- tooltip、临时窗口等行为异常。
- 对象生命周期比预期更长。
15.4 长任务的三个更好替代方案
工作线程
适合 CPU 密集或阻塞工作:
text
GUI 发请求
worker 计算
worker 发结果
GUI 更新
分片状态机
适合可拆分任务:
cpp
void Processor::processNextBatch()
{
processAtMost100Items();
if (hasMoreItems())
QTimer::singleShot(0, this, &Processor::processNextBatch);
}
每批有上限,事件循环有机会处理其他工作。
真异步 API
网络、进程、I/O 优先使用 Qt 的异步接口,通过 readyRead、finished 等信号推进状态,不要在 GUI 线程同步等待。
16. QObject 的线程亲和性
每个 QObject 都有一个所属线程:
cpp
qDebug() << object->thread();
qDebug() << QThread::currentThread();
线程亲和性决定:
- 排队信号槽在哪个线程执行。
postEvent()投递到哪个线程队列。- 定时器和 socket notifier 由哪个线程事件循环处理。
deleteLater()的延迟删除在哪里发生。
它不代表:
- 对象自动加锁。
- 其他线程无法拿到指针。
- 所有成员函数调用都会自动切换线程。
普通 C++ 调用永远在调用者当前线程执行:
cpp
worker->doWork(); // 不会因为 worker 属于工作线程就自动切线程
要切换到 worker 线程,应使用:
cpp
QMetaObject::invokeMethod(
worker,
&Worker::doWork,
Qt::QueuedConnection);
或排队信号槽。
17. moveToThread() 的规则
cpp
worker->moveToThread(thread);
主要规则:
- 对象有 parent 时不能移动。
- 子对象会随父对象一起改变线程亲和性。
- 普通成员指针指向的对象如果没有设置 parent,不会自动跟随。
- 通常只能从对象当前所属线程把它"推"到另一个线程。
- GUI 对象必须留在 GUI 主线程。
错误示例:
cpp
Worker *worker = new Worker(this); // 有 parent
worker->moveToThread(thread); // 失败
正确思路:
cpp
Worker *worker = new Worker; // 暂不设 parent
worker->moveToThread(thread);
之后通过线程结束信号和 deleteLater() 管理生命周期。
18. QThread 对象不生活在它管理的线程里
这是 Qt 多线程最常见的误区。
cpp
QThread *thread = new QThread(this);
thread->start();
这里有两个不同实体:
thread这个QThread对象通常生活在创建它的线程,例如 GUI 线程。thread->run()管理的代码在新操作系统线程中执行。
因此,给 QThread 子类添加槽并不意味着槽自动在工作线程执行。槽的执行仍根据:
QThread对象自身的线程亲和性。- 连接类型。
- 发信线程。
对于需要事件循环和多个槽的长期 worker,推荐 worker-object 模式。
19. 推荐的 worker-object 模式
19.1 Worker
cpp
class Worker : public QObject
{
Q_OBJECT
public:
// 这是显式设计为可跨线程调用的线程安全函数,只操作原子变量。
void requestStop() noexcept
{
m_stopRequested.store(true, std::memory_order_relaxed);
}
public slots:
void start(Task task)
{
Result result = performTask(task, m_stopRequested);
if (m_stopRequested.load(std::memory_order_relaxed))
return;
emit resultReady(std::move(result));
}
signals:
void resultReady(Result result);
void failed(QString reason);
private:
std::atomic_bool m_stopRequested = false;
};
19.2 Controller 中创建线程
cpp
class Controller : public QObject
{
Q_OBJECT
public:
explicit Controller(QObject *parent = nullptr)
: QObject(parent),
m_thread(new QThread(this)),
m_worker(new Worker)
{
m_worker->moveToThread(m_thread);
connect(
this,
&Controller::startRequested,
m_worker,
&Worker::start,
Qt::QueuedConnection);
connect(
m_worker,
&Worker::resultReady,
this,
&Controller::handleResult);
connect(
m_thread,
&QThread::finished,
m_worker,
&QObject::deleteLater);
m_thread->start();
}
~Controller() override
{
// requestStop() 有明确的线程安全契约;普通 QObject 方法不能照此类推。
m_worker->requestStop();
m_thread->quit();
m_thread->wait();
}
signals:
void startRequested(Task task);
private slots:
void handleResult(Result result);
private:
QThread *m_thread;
Worker *m_worker;
};
19.3 这个模式为什么成立
Controller和QThread对象生活在控制线程。Worker被移动到工作线程。startRequested排队到 worker 线程执行。resultReady自动排队回 controller 线程。requestStop()只修改原子标志,长任务在有限间隔内主动检查。quit()请求工作线程事件循环退出。wait()保证析构前操作系统线程结束。finished -> deleteLater让 worker 在线程结束路径中安全清理。
实际工程还应处理:
- 任务是否允许中断。
performTask()长时间运行时,排队的stop()无法被处理。- 异常如何转换为错误信号。
- 重复启动、超时和关闭顺序。
20. 协作取消:为什么一个 stop() 槽可能不够
如果 worker 正在一个长循环中,线程事件循环无法处理排队的 stop():
cpp
void Worker::start()
{
while (true) {
computeForLongTime(); // 一直不返回事件循环
}
}
此时 stopRequested -> Worker::stop 虽然已排队,却没有机会执行。
可选方案:
20.1 原子取消标志
cpp
class Worker : public QObject
{
public:
void requestStop() noexcept
{
m_stopRequested.store(true, std::memory_order_relaxed);
}
public slots:
void start()
{
while (!m_stopRequested.load(std::memory_order_relaxed)) {
performOneBoundedStep();
}
}
private:
std::atomic_bool m_stopRequested = false;
};
这里 requestStop() 可设计为线程安全普通函数,但必须清楚这是自行建立的并发协议,不是 QObject 自动保证。
20.2 分块任务
每次处理有限工作,然后用定时器或排队调用继续,使事件循环有机会处理取消和其他消息。
20.3 QThread::requestInterruption()
在线程任务中定期检查:
cpp
while (!QThread::currentThread()->isInterruptionRequested()) {
performOneBoundedStep();
}
它只是协作取消标志,不会强制终止代码。
不要使用强制线程终止来解决常规退出问题,因为线程可能正持有锁、操作内存或执行库代码。
21. 什么时候继承 QThread
继承 QThread 不是绝对错误,适合:
- 线程只执行一个清晰、阻塞式的计算过程。
- 不需要该工作线程中的 QObject 事件循环。
- 需要自定义
run()初始化和退出流程。
cpp
class HashThread : public QThread
{
Q_OBJECT
protected:
void run() override
{
const QByteArray result = calculateHash();
emit resultReady(result);
}
signals:
void resultReady(QByteArray result);
};
要记住:
run()在新线程。HashThread对象的普通槽通常仍在创建线程。- 从
run()发信号是常见且安全的用法。 - 如果覆盖
run()且不调用exec(),该线程没有 Qt 事件循环。
22. 无事件循环时哪些功能失效
如果某线程没有运行事件循环:
- 投递给该线程对象的排队槽不会正常执行。
postEvent()的普通事件不会被分发。QTimer不会正常触发。- socket notifier 不工作。
deleteLater()不会按常规事件轮次执行;对象通常要等线程结束时清理。
因此下面的代码有问题:
cpp
class Thread : public QThread
{
protected:
void run() override
{
QTimer timer;
timer.start(1000);
// 没有 exec(),timer 没有事件循环可依赖。
}
};
如果需要定时器:
cpp
void run() override
{
QTimer timer;
connect(&timer, &QTimer::timeout, [] {
qDebug() << "tick";
});
timer.start(1000);
exec();
}
更常见的做法仍是使用 worker-object,让默认 QThread::run() 提供事件循环。
23. 跨线程通信的推荐方式
23.1 排队信号槽
最自然的一对多通知:
cpp
connect(
producer,
&Producer::frameReady,
consumer,
&Consumer::consumeFrame,
Qt::QueuedConnection);
23.2 QMetaObject::invokeMethod
一次性命令:
cpp
QMetaObject::invokeMethod(
worker,
[worker, config] {
worker->applyConfig(config);
},
Qt::QueuedConnection);
提供 context/receiver 后,调用进入其线程,且对象销毁后不会再正常调用目标。
23.3 postEvent
适合目标明确、需要优先级或事件过滤的消息。
23.4 线程安全队列
高吞吐、背压或批处理场景可能需要显式队列:
- 互斥锁 + 条件变量。
- 有界队列。
- 单生产者单消费者队列。
- 批量交换缓冲区。
信号可以只负责"队列中有新数据"的轻量唤醒,数据本身留在受控队列中。
24. 自定义类型跨线程传递
cpp
struct FrameResult
{
quint64 sequence = 0;
QImage image;
QStringList labels;
};
Q_DECLARE_METATYPE(FrameResult)
初始化:
cpp
qRegisterMetaType<FrameResult>("FrameResult");
信号:
cpp
signals:
void frameReady(FrameResult result);
设计建议:
- 消息尽量不可变。
- 使用值语义,降低共享可变状态。
- 大对象评估复制成本;Qt 的许多容器和图像类型使用隐式共享。
- 不要跨线程传递指向发送者内部缓存的裸指针。
- 共享所有权不等于线程安全,多个线程修改同一个 pointee 仍需同步。
25. GUI 线程规则
QWidget、QWindow 及绝大多数 GUI 对象必须只在主 GUI 线程创建和操作。
错误:
cpp
void Worker::process()
{
label->setText("done"); // worker 线程直接访问 QWidget
}
正确:
cpp
emit progressTextChanged("done");
connect(
worker,
&Worker::progressTextChanged,
label,
&QLabel::setText);
label 在 GUI 线程,跨线程 AutoConnection 会成为排队连接。
对于图像:
QImage适合在工作线程进行像素处理,但共享写入仍要同步。QPixmap与窗口系统资源关系更紧密,通常应在 GUI 线程使用。
26. deleteLater() 为什么重要
直接跨线程删除一个正在处理事件的 QObject 可能造成 use-after-free。
cpp
worker->deleteLater();
会向对象所属线程安排 DeferredDelete 事件,使删除发生在合适的事件循环阶段。
注意:
- 它不是立即删除。
- 主事件循环已经停止后调用,不能期待主循环再次处理它。
- 对象线程无运行事件循环时,删除时机依赖线程结束等特殊处理。
- 调用后应把业务上持有的裸指针视为即将失效。
可以使用 QPointer:
cpp
QPointer<Worker> guardedWorker = worker;
worker->deleteLater();
if (guardedWorker) {
// 尚未实际析构
}
26.1 deleteLater() 源码上做了什么
它不会调用:
cpp
delete this;
而是概念上:
- 检查是否已经安排过延迟删除。
- 记录当前线程的事件循环层级和事件处理 scope。
- 创建
QDeferredDeleteEvent。 postEvent(this, deferredDeleteEvent)。
之后事件循环处理该事件时,QObject::event() 中的分支执行:
cpp
case QEvent::DeferredDelete:
delete this;
break;
所以删除最终仍由 delete 完成,只是执行点被推迟到对象所属线程合适的事件循环阶段。
26.2 为什么不是下一条任意事件就删除
考虑:
cpp
void Receiver::slot()
{
deleteLater();
continueUsingMembers();
}
如果 deleteLater() 立即删除,当前槽还没返回就会访问已销毁对象。
Qt 记录 loop/scope level,确保延迟删除不会在当前调用栈不安全地重入执行。
正确心智:
text
deleteLater()
表示"当前对象应退出生命周期"
但允许当前正在执行的事件/槽安全返回
调用后仍不应继续安排新的长期业务工作。
26.3 为什么嵌套循环使删除更复杂
外层事件处理中:
cpp
object->deleteLater();
dialog.exec(); // 进入更深一层循环
如果内层循环随意处理外层安排的删除,外层函数返回时可能继续访问已删除对象。
Qt 的 loop level 规则就是为了避免某些不安全的过早删除,同时保证退出相应循环时最终能够清理。
26.4 finished -> deleteLater 为什么常见
cpp
connect(
thread,
&QThread::finished,
worker,
&QObject::deleteLater);
它表达:
text
线程结束流程发生
↓
安排 worker 在其线程生命周期结束路径中清理
这比控制线程直接:
cpp
delete worker;
更符合对象线程亲和性。
但线程关闭顺序仍要完整:
text
请求任务停止
等待长任务协作退出
quit 事件循环
wait 操作系统线程结束
清理控制对象
27. 常见线程错误
错误 1:直接调用已移动对象的方法
cpp
worker->start(task); // 在当前线程执行
修复:排队信号或 invokeMethod()。
错误 2:认为 AutoConnection 总由 sender/receiver 的 affinity 决定
实际要看发出信号的当前线程和 receiver affinity。
错误 3:跨线程使用 DirectConnection
槽在发信线程运行,可能破坏对象的单线程假设。
错误 4:线程退出时不 wait()
控制对象析构后,后台线程仍访问其状态,或出现:
text
QThread: Destroyed while thread is still running
错误 5:把有 parent 的 worker 移动到线程
moveToThread() 会失败,worker 仍留在原线程。
错误 6:线程没有事件循环
排队槽和 timer 不执行;deleteLater() 也不会按正常事件轮次处理,通常要等线程结束。
错误 7:长槽阻塞线程事件循环
worker 虽然不阻塞 UI,但该 worker 线程中的其他命令、取消和定时器仍会饥饿。
错误 8:使用共享指针后认为无需同步
智能指针只管理生命周期,不自动保护对象内容。
28. 信号设计与背压
高频生产者可能比消费者更快:
text
摄像头 120 FPS -> 算法只能处理 30 FPS
如果每帧都排队,事件队列会持续增长,表现为:
- 延迟越来越大。
- 内存上升。
- 用户看到很旧的结果。
- 停止操作迟迟不能执行。
可选策略:
28.1 只保留最新值
生产者覆盖共享的"最新帧",消费者每次取当前最新数据。
28.2 有界队列
达到容量后:
- 丢弃最旧。
- 丢弃最新。
- 阻塞生产者。
- 降采样。
策略必须根据实时性和完整性要求选择。
28.3 合并通知
队列从空变为非空时只发一次唤醒信号,消费者批量取走数据,避免每条数据对应一个元调用事件。
28.4 请求编号
UI 快速连续发起搜索时,只应用最后一次请求的结果。
Qt 的排队连接提供运输机制,不提供业务背压策略。
29. 信号、事件和锁如何配合
不要在持锁时发出外部可见信号
cpp
void Model::setValue(int value)
{
QMutexLocker locker(&m_mutex);
m_value = value;
emit valueChanged(value); // 槽可能同步回调并再次获取 m_mutex
}
如果连接是直接连接,槽会在锁未释放时立即执行,可能:
- 死锁。
- 调用未知外部代码。
- 让临界区持续时间不可控。
更稳妥:
cpp
void Model::setValue(int value)
{
{
QMutexLocker locker(&m_mutex);
if (m_value == value)
return;
m_value = value;
}
emit valueChanged(value);
}
但还要判断释放锁后状态是否允许其他线程再次改变。复杂状态应通过消息串行化或明确的状态快照设计。
不要假设排队连接可以替代所有锁
如果所有可变状态只在一个线程访问,消息传递确实能显著减少锁。
一旦多个线程仍直接读写同一数据,排队信号不会自动消除数据竞争。
30. 诊断与调试方法
30.1 打印执行线程
cpp
qDebug() << Q_FUNC_INFO
<< "current:" << QThread::currentThread()
<< "affinity:" << thread();
遇到"槽为什么在这个线程运行"时,分别打印:
- 发信位置的
QThread::currentThread()。 - 槽执行位置的
QThread::currentThread()。 - receiver 的
thread()。 - 实际连接类型。
30.2 检查连接结果
cpp
const QMetaObject::Connection connection = connect(...);
Q_ASSERT(connection);
对 UniqueConnection,无效句柄可能表示重复连接。
30.3 检查对象是否真的移动成功
cpp
worker->moveToThread(thread);
Q_ASSERT(worker->thread() == thread);
同时查看运行日志中是否有:
text
QObject::moveToThread: Cannot move objects with a parent
30.4 检查事件循环
- 线程是否调用了
start()? - 是否覆盖
run()却忘了exec()? - 槽是否长时间不返回?
- 是否在
quit()后继续投递任务? QTimer是否在对象所属线程启动和停止?
30.5 检查排队类型
出现:
text
QObject::connect: Cannot queue arguments of type 'MyType'
检查:
Q_DECLARE_METATYPE(MyType)qRegisterMetaType<MyType>()- 注册是否发生在建立连接和第一次使用之前
- 类型名和命名空间是否一致
- 类型是否真正可复制/移动和安全析构
30.6 使用断言保护线程边界
cpp
void Worker::process(Task task)
{
Q_ASSERT(QThread::currentThread() == thread());
// ...
}
对只能在 GUI 线程调用的控制器也可以做类似检查。
31. 常见现象与根因对照
| 现象 | 常见根因 |
|---|---|
| 信号发了,槽不执行 | 未连接、context 已销毁、目标线程无事件循环、签名不匹配 |
| 槽偶尔在错误线程 | 强制 Direct、直接调用已 move 的对象、误解 Auto 判断规则 |
| UI 卡死 | 主线程长槽、BlockingQueued 死锁、持锁等待 worker |
| worker 的 stop 不响应 | 长任务不返回事件循环 |
| 内存持续上涨 | 高频 queued 消息无背压、结果积压 |
| disconnect 后槽还执行一次 | 事件在断开前已排队 |
| timer 不触发 | 在线程中无事件循环,或从错误线程启动/停止 |
moveToThread 无效 |
对象有 parent,或从错误线程移动 |
| 关闭程序崩溃 | 线程未 quit/wait、异步回调捕获悬垂指针 |
| 重复处理一次信号 | 重复 connect,未保存/管理连接 |
32. 面试高频问题
问:信号发出后槽在哪个线程执行?
取决于连接类型。Direct 在发信号的当前线程立即执行;Queued 把调用投递到 receiver 所属线程的事件循环;Auto 在发信时比较当前发信线程和 receiver 的线程亲和性,同线程走 Direct,否则走 Queued;BlockingQueued 在 receiver 线程执行,但发信线程等待槽返回,同线程使用会死锁。
问:信号槽一定依赖事件循环吗?
不一定。Direct 连接和同线程 Auto 连接像普通函数调用,不依赖事件循环。Queued、BlockingQueued 以及跨线程 Auto 需要目标线程事件循环来取出元调用事件。
问:QThread 的槽为什么可能在主线程执行?
因为
QThread对象本身通常创建并生活在主线程,新线程执行的是run()。给QThread对象连接槽时,槽的线程仍按这个对象的 affinity 和连接类型决定。需要工作线程中的槽时,通常创建独立 worker 并moveToThread()。
问:sendEvent 和 postEvent 的区别?
sendEvent当前线程同步分发,不转移事件所有权;postEvent线程安全地把堆事件投递到 receiver 所属线程,Qt 接管事件所有权,事件循环稍后处理。前者容易产生当前栈重入,后者依赖目标事件循环。
问:事件过滤器返回 true 是什么意思?
表示事件被过滤器消费,停止继续发送给后续过滤器和目标对象。返回 false 才继续正常分发。多个过滤器按后安装先执行,而且过滤器和被观察对象必须在同一线程。
问:排队信号槽为什么要求注册自定义类型?
因为参数必须脱离发送者当前调用栈保存到事件中,目标线程稍后再恢复和析构。Qt 需要通过元类型系统知道参数的类型信息及复制、移动、销毁方式。
问:disconnect 后为什么槽还可能调用?
因为断开只移除连接关系;断开之前已经生成并投递的元调用事件可能仍在目标线程队列中。需要业务级取消时还要使用请求编号、状态检查或取消令牌。
33. 工程设计清单
信号槽
- 新代码优先成员函数指针或带 context 的 lambda。
- 明确基础连接类型,不把 Direct 当跨线程捷径。
- 保存需要主动断开的
QMetaObject::Connection。 - 防止重复 connect;成员槽可考虑
UniqueConnection。 - 一次性观察使用
SingleShotConnection。 - 不在持锁时发出会进入未知外部代码的信号。
事件
- 优先重写最具体的事件处理函数。
- 过滤器只处理横切或无法修改目标类的场景。
- 未处理时调用基类实现。
postEvent后不再访问事件指针。- 避免嵌套事件循环和滥用
processEvents()。
线程
- GUI 对象只在主线程访问。
- 跨线程通过排队消息,不直接调用 worker。
- 线程启动、停止、取消和析构形成完整闭环。
- 长任务设计协作取消点。
- 跨线程参数使用拥有型值语义。
- 对高频消息设计有界队列或合并策略。
- 析构前
quit()+wait(),不要销毁仍运行的QThread。
34. 动手实验与自测
34.1 实验一:Direct 槽是否阻塞 emit
cpp
class Sender : public QObject
{
Q_OBJECT
signals:
void started();
};
Sender sender;
QObject::connect(
&sender,
&Sender::started,
&sender,
[] {
qDebug() << "slot begin";
QThread::sleep(2);
qDebug() << "slot end";
},
Qt::DirectConnection);
qDebug() << "before emit";
emit sender.started();
qDebug() << "after emit";
预测:
text
before emit
slot begin
等待两秒
slot end
after emit
结论:
Direct 信号槽处在同一调用栈,
emit要等槽返回。
34.2 实验二:Queued 槽何时执行
cpp
QObject::connect(
&sender,
&Sender::started,
&receiver,
&Receiver::run,
Qt::QueuedConnection);
qDebug() << "before emit";
emit sender.started();
qDebug() << "after emit";
在 Receiver::run() 中打印:
cpp
qDebug() << "queued slot";
主事件循环运行时,通常看到:
text
before emit
after emit
queued slot
如果在 app.exec() 前发出且立即结束程序,槽可能没有机会执行。原因不是连接失败,而是排队事件没有被事件循环取出。
34.3 实验三:AutoConnection 看谁的线程
分别打印:
cpp
qDebug() << "emit current:"
<< QThread::currentThread();
qDebug() << "sender affinity:"
<< sender.thread();
qDebug() << "receiver affinity:"
<< receiver.thread();
在槽中:
cpp
qDebug() << "slot current:"
<< QThread::currentThread();
设计三组情况:
- sender、receiver 和发信代码都在主线程。
- receiver 移到工作线程,从主线程发信号。
- sender affinity 在工作线程,却错误地从主线程直接调用 sender 方法并发信号。
观察第三种情况,理解 Auto 判断的是当前发信线程与 receiver affinity,不是简单比较 sender affinity。
34.4 实验四:多个直接槽的建立顺序
cpp
connect(&sender, &Sender::started, [] {
qDebug() << "A";
});
connect(&sender, &Sender::started, [] {
qDebug() << "B";
});
connect(&sender, &Sender::started, [] {
qDebug() << "C";
});
emit sender.started();
通常输出:
text
A
B
C
然后在 A 中新增 D:
cpp
connect(&sender, &Sender::started, [&] {
qDebug() << "A";
connect(&sender, &Sender::started, [] {
qDebug() << "D";
});
});
观察 D 是否参与第一次发射,以及第二次发射时的位置。
这个实验对应源码中的 connection id 批次边界。
34.5 实验五:断开后已排队调用是否仍可能执行
cpp
QMetaObject::Connection connection = connect(
&sender,
&Sender::started,
&receiver,
&Receiver::run,
Qt::QueuedConnection);
emit sender.started();
disconnect(connection);
让事件循环继续运行,观察 Receiver::run() 是否仍可能执行。
不要把单次实验结果误认为所有版本和所有竞争时机的绝对保证。要掌握的工程结论是:
disconnect()防止通过该连接继续产生调用,但不能作为撤销所有已生成异步消息的业务取消协议。
34.6 实验六:事件过滤器的真实顺序
创建三个过滤器:
cpp
target->installEventFilter(filterA);
target->installEventFilter(filterB);
target->installEventFilter(filterC);
每个过滤器打印名称并返回 false。
预期顺序:
text
C
B
A
target event
再让 B 返回 true:
text
C
B
A 和 target 都不再收到事件。
34.7 实验七:验证 posted event 优先级和 FIFO
定义带名称的自定义事件:
cpp
QCoreApplication::postEvent(
&receiver,
new NamedEvent("normal-1"),
Qt::NormalEventPriority);
QCoreApplication::postEvent(
&receiver,
new NamedEvent("high"),
Qt::HighEventPriority);
QCoreApplication::postEvent(
&receiver,
new NamedEvent("normal-2"),
Qt::NormalEventPriority);
QCoreApplication::postEvent(
&receiver,
new NamedEvent("low"),
Qt::LowEventPriority);
预期 posted-event 队列内部顺序:
text
high
normal-1
normal-2
low
这个实验验证:
- priority 降序。
- 同 priority FIFO。
34.8 实验八:本轮事件中新 post 的事件
cpp
void Receiver::customEvent(QEvent *event)
{
auto *named = static_cast<NamedEvent *>(event);
qDebug() << named->name();
if (named->name() == "first") {
QCoreApplication::postEvent(
this,
new NamedEvent("posted-inside-first"));
}
}
先投递:
cpp
postEvent(receiver, new NamedEvent("first"));
postEvent(receiver, new NamedEvent("second"));
观察:
text
first
second
posted-inside-first
这个结果体现 sendPostedEvents() 的当前批次边界。实际实验还可能夹杂其他来源事件,所以只分析这些自定义事件之间的相对关系。
34.9 实验九:sendEvent() 与 postEvent() 调用栈
在调用前后打印:
cpp
qDebug() << "before send";
QEvent event(MyType);
QCoreApplication::sendEvent(&receiver, &event);
qDebug() << "after send";
qDebug() << "before post";
QCoreApplication::postEvent(
&receiver,
new QEvent(MyType));
qDebug() << "after post";
在 receiver.event() 中打印。
应观察到:
text
before send
receiver event
after send
before post
after post
receiver event
34.10 实验十:观察 QEvent::MetaCall
给 receiver 安装事件过滤器:
cpp
bool Filter::eventFilter(QObject *watched, QEvent *event)
{
qDebug() << watched << event->type();
return false;
}
向 receiver 建立 QueuedConnection 并发信号。
观察事件类型中是否出现 QEvent::MetaCall。不要返回 true 过滤它,否则会阻止排队槽执行。
这个实验把下面链路连起来:
text
QueuedConnection
-> 元调用事件
-> 对象事件过滤器
-> QObject::event
-> 槽
34.11 实验十一:阻塞事件循环
在 GUI 槽中:
cpp
void MainWindow::freeze()
{
qDebug() << "freeze begin";
QThread::sleep(5);
qDebug() << "freeze end";
}
同时启动一个主线程 QTimer 和一个从 worker 排队回主线程的信号。
观察五秒期间:
- 窗口不重绘。
- timer 不执行。
- worker 信号已经发出,但主线程槽不执行。
- 五秒后多个工作集中恢复。
结论:
工作可以在别的线程完成,但结果仍要等接收线程事件循环空闲。
34.12 实验十二:分片任务替代 processEvents()
反模式:
cpp
for (int i = 0; i < items.size(); ++i) {
process(items[i]);
QCoreApplication::processEvents();
}
改成:
cpp
void Processor::processBatch()
{
const int end = qMin(m_index + 100, m_items.size());
while (m_index < end)
process(m_items[m_index++]);
if (m_index < m_items.size()) {
QTimer::singleShot(
0,
this,
&Processor::processBatch);
} else {
emit finished();
}
}
比较:
- 状态是否更容易推理。
- 是否还需要担心当前函数中间对象被任意事件删除。
- 取消逻辑是否能在每批之间处理。
34.13 实验十三:观察 deleteLater()
cpp
QObject *object = new QObject;
QPointer<QObject> guard = object;
connect(object, &QObject::destroyed, [] {
qDebug() << "destroyed";
});
qDebug() << "before deleteLater:" << bool(guard);
object->deleteLater();
qDebug() << "after deleteLater:" << bool(guard);
QTimer::singleShot(0, [] {
qDebug() << "next turn";
});
观察:
deleteLater()返回时对象通常还存在。- 返回事件循环后才执行延迟删除。
- 精确相对 0ms timer 的顺序不应写成业务依赖。
34.14 实验十四:不要猜 timer、queued slot 的总顺序
几乎同时安排:
cpp
QTimer::singleShot(0, [] {
qDebug() << "timer";
});
QMetaObject::invokeMethod(
&receiver,
[] {
qDebug() << "queued invoke";
},
Qt::QueuedConnection);
在不同平台、不同投递先后和不同上下文中运行。
正确学习目标不是背某次输出,而是理解:
两者都依赖事件分发,但不同事件源之间没有适合作为业务协议的跨平台固定总顺序。
需要固定顺序时:
cpp
QMetaObject::invokeMethod(
&receiver,
[] {
first();
second();
},
Qt::QueuedConnection);
把顺序放在同一个串行任务中。
34.15 源码阅读路线
阅读 Qt 6 源码时按这条路径,不要从头硬啃整个文件:
text
信号槽:
moc 生成的信号函数
-> QMetaObject::activate
-> doActivate
-> queued_activate
-> QQueuedMetaCallEvent
-> QObject::event(QEvent::MetaCall)
-> QMetaCallEvent::placeMetaCall
posted event:
QCoreApplication::postEvent
-> QPostEventList::addEvent
-> eventDispatcher->wakeUp
-> sendPostedEvents
-> QCoreApplication::sendEvent
-> notify_helper
-> receiver->event
事件循环:
QCoreApplication::exec
-> QEventLoop::exec
-> QAbstractEventDispatcher::processEvents
-> 平台后端
建议每次只回答一个问题:
text
这个函数的输入是什么?
它把状态保存在哪里?
下一跳调用谁?
在哪个线程执行?
锁在何时释放?
用户代码可能在哪里重入?
34.16 自测题
emit是否一定异步?connect()内部为什么要保存 signal index?- 一条连接为什么要同时挂到 sender 和 receiver?
- AutoConnection 在连接时还是发射时决定 Direct/Queued?
- QueuedConnection 为什么必须复制参数?
QMetaCallEvent最终在哪里恢复成槽调用?- BlockingQueued 为什么同线程必然死锁?
- posted event 的优先级和 FIFO 规则是什么?
- timer、socket、queued slot 是否存在跨平台固定总顺序?
- 应用级和对象级过滤器谁先?
- 同一对象多个过滤器按什么顺序?
event()和mousePressEvent()是什么关系?- 当前事件处理中再次 post 的事件为什么通常留到下一批?
deleteLater()为什么不立即删除?disconnect()为什么不能作为完整异步取消协议?
34.17 参考答案
- 不一定;Direct 同步执行,Queued 才依赖事件队列。
- 用整数快速定位该信号的连接列表,避免反复字符串查找。
- sender 发射时需要正向查找,receiver 析构时需要反向清理。
- 发射时,根据当前发信线程和 receiver affinity 判断。
- 原始调用栈返回后参数可能销毁,事件必须拥有可独立存活的值。
- 目标线程取出 MetaCall 事件后,经
QObject::event()调用placeMetaCall()。 - 线程阻塞等待自己处理队列中的事件,但事件循环已无法运行。
- 高 priority 先;相同 priority 按投递顺序。
- 没有适合作为业务依赖的统一固定总顺序,后端和时机都会影响。
- 应用级过滤器先。
- 后安装的先执行。
event()根据事件类型分发到专用处理函数。- 事件循环设置当前批次边界,避免自投递造成活锁和其他来源饥饿。
- 要允许当前事件/槽安全返回,并在对象所属线程合适的循环层级销毁。
- 已经形成并投递的异步事件可能独立于连接记录继续存在。
35. 参考资料
- Signals & Slots: https://doc.qt.io/qt-6/signalsandslots.html
- Qt ConnectionType: https://doc.qt.io/qt-6/qt.html#ConnectionType-enum
- The Event System: https://doc.qt.io/qt-6/eventsandfilters.html
- QObject: https://doc.qt.io/qt-6/qobject.html
- QCoreApplication: https://doc.qt.io/qt-6/qcoreapplication.html
- QEventLoop: https://doc.qt.io/qt-6/qeventloop.html
- Threads and QObjects: https://doc.qt.io/qt-6/threads-qobject.html
- QThread: https://doc.qt.io/qt-6/qthread.html
- Synchronizing Threads: https://doc.qt.io/qt-6/threads-synchronizing.html
- QMetaObject: https://doc.qt.io/qt-6/qmetaobject.html
- QAbstractEventDispatcher: https://doc.qt.io/qt-6/qabstracteventdispatcher.html
- Qt
qobject.cpp官方源码: https://github.com/qt/qtbase/blob/dev/src/corelib/kernel/qobject.cpp - Qt
qcoreapplication.cpp官方源码: https://github.com/qt/qtbase/blob/dev/src/corelib/kernel/qcoreapplication.cpp - Qt
qeventloop.cpp官方源码: https://github.com/qt/qtbase/blob/dev/src/corelib/kernel/qeventloop.cpp - Qt Unix event dispatcher 源码: https://github.com/qt/qtbase/blob/dev/src/corelib/kernel/qeventdispatcher_unix.cpp
- Qt Win32 event dispatcher 源码: https://github.com/qt/qtbase/blob/dev/src/corelib/kernel/qeventdispatcher_win.cpp
- Qt
qobject.cpp源码索引: https://codebrowser.dev/qt6/qtbase/src/corelib/kernel/qobject.cpp.html