Qt 信号槽、事件循环与线程通信源码级理解

Qt 信号槽、事件循环与线程通信:从使用到源码

本文从普通 C++ 回调和消息队列开始,逐步讲解 Qt 信号槽的源码实现、事件循环如何等待和分发事件、事件过滤器的调用顺序,以及 QObject 在线程之间如何安全通信。

全文重点回答:

  1. connect() 建立连接时,Qt 内部保存了什么?
  2. emit 之后为什么能找到一个或多个槽?
  3. Direct、Queued 和 BlockingQueued 在源码中从哪里分叉?
  4. 排队调用如何复制参数,并在接收线程恢复成真实函数调用?
  5. app.exec() 到底是不是一个简单的 while 循环?
  6. posted event、系统消息、定时器、socket notifier 到底谁先处理?
  7. 事件过滤器、notify()、event() 和专用事件函数的先后顺序是什么?
  8. 为什么 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 事件处理顺序是什么"时,不能只给一个绝对列表。需要区分:

  1. 同一个 posted-event 队列内部
    • 高优先级先处理。
    • 相同优先级按投递顺序处理。
  2. 一个事件交给对象后的分发顺序
    • 应用级过滤器。
    • 对象级过滤器。
    • receiver->event()。
    • 专用事件处理函数。
  3. 不同事件来源之间
    • 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);
});

这段代码揭示排队调用的三个关键要求:

  1. receiver 执行时必须仍然有效。
  2. result 必须脱离发送者当前栈保存下来。
  3. 队列必须唤醒目标线程处理任务。

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

信号发出时不会立即执行槽,而是:

  1. 读取连接和参数元类型。
  2. 复制或封送参数。
  3. 创建一次元调用事件。
  4. 投递到接收对象所属线程的事件队列。
  5. 发信线程继续执行。
  6. 目标线程事件循环稍后取出事件并调用槽。
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 建立连接的完整概念流程

现代成员函数指针连接大致经历:

  1. 模板代码在编译期确认信号和槽参数兼容。
  2. 将信号成员函数映射为信号索引。
  3. 把槽成员函数或 functor 封装成统一可调用对象。
  4. 锁定发送者和接收者的信号槽内部数据。
  5. 处理 UniqueConnection 和 SingleShotConnection 标志。
  6. 创建连接记录。
  7. 缓存接收对象的线程数据。
  8. 将记录挂入发送者按信号组织的连接列表。
  9. 在接收者侧记录反向关系,便于析构时清理。
  10. 返回 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 源码中的核心工作:

  1. 取得信号参数的元类型列表。
  2. 确认所有参数都可以排队保存。
  3. 重新确认接收者仍存在。
  4. 创建 QQueuedMetaCallEvent。
  5. 使用元类型复制每个参数。
  6. 处理 single-shot。
  7. 再次确认连接没有在复制期间被断开。
  8. 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 一轮事件循环的通用阶段

跨平台可以把一轮理解为:

  1. 标记事件分发器处于唤醒状态。
  2. 分发当前允许处理的 posted events。
  3. 根据 flags 决定是否包括用户输入、socket notifier、timer。
  4. 判断当前是否还有立即可做的工作。
  5. 如果没有并允许等待,发出 aboutToBlock()。
  6. 进入平台等待。
  7. 因系统消息、I/O、timer 到期或 wakeUp() 返回。
  8. 发出/进入 awake 状态。
  9. 激活准备好的平台事件、socket notifier 和 timer。
  10. 返回上层 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 源码中的核心步骤:

  1. 锁定当前线程的 posted-event 列表。
  2. 记录递归深度和当前批次插入边界。
  3. 找到符合 receiver/type 条件的事件。
  4. 对 DeferredDelete 做特殊层级判断。
  5. 从列表中逻辑移除事件。
  6. 解锁队列。
  7. 调用 QCoreApplication::sendEvent(receiver, event)。
  8. sendEvent() 进入 notify() 分发链。
  9. 事件处理后删除事件对象。
  10. 重新加锁继续下一个。

为什么调用用户事件处理函数前要解锁?

因为用户代码可以:

  • 再次 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;

而是概念上:

  1. 检查是否已经安排过延迟删除。
  2. 记录当前线程的事件循环层级和事件处理 scope。
  3. 创建 QDeferredDeleteEvent。
  4. 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();

设计三组情况:

  1. sender、receiver 和发信代码都在主线程。
  2. receiver 移到工作线程,从主线程发信号。
  3. 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 自测题

  1. emit 是否一定异步?
  2. connect() 内部为什么要保存 signal index?
  3. 一条连接为什么要同时挂到 sender 和 receiver?
  4. AutoConnection 在连接时还是发射时决定 Direct/Queued?
  5. QueuedConnection 为什么必须复制参数?
  6. QMetaCallEvent 最终在哪里恢复成槽调用?
  7. BlockingQueued 为什么同线程必然死锁?
  8. posted event 的优先级和 FIFO 规则是什么?
  9. timer、socket、queued slot 是否存在跨平台固定总顺序?
  10. 应用级和对象级过滤器谁先?
  11. 同一对象多个过滤器按什么顺序?
  12. event() 和 mousePressEvent() 是什么关系?
  13. 当前事件处理中再次 post 的事件为什么通常留到下一批?
  14. deleteLater() 为什么不立即删除?
  15. disconnect() 为什么不能作为完整异步取消协议?

34.17 参考答案

  1. 不一定;Direct 同步执行,Queued 才依赖事件队列。
  2. 用整数快速定位该信号的连接列表,避免反复字符串查找。
  3. sender 发射时需要正向查找,receiver 析构时需要反向清理。
  4. 发射时,根据当前发信线程和 receiver affinity 判断。
  5. 原始调用栈返回后参数可能销毁,事件必须拥有可独立存活的值。
  6. 目标线程取出 MetaCall 事件后,经 QObject::event() 调用 placeMetaCall()。
  7. 线程阻塞等待自己处理队列中的事件,但事件循环已无法运行。
  8. 高 priority 先;相同 priority 按投递顺序。
  9. 没有适合作为业务依赖的统一固定总顺序,后端和时机都会影响。
  10. 应用级过滤器先。
  11. 后安装的先执行。
  12. event() 根据事件类型分发到专用处理函数。
  13. 事件循环设置当前批次边界,避免自投递造成活锁和其他来源饥饿。
  14. 要允许当前事件/槽安全返回,并在对象所属线程合适的循环层级销毁。
  15. 已经形成并投递的异步事件可能独立于连接记录继续存在。

35. 参考资料

相关推荐
ESDWAN12 小时前
海外股票网络环境优化与交易场景应用:从行情获取到交易系统稳定性设计
开发语言·网络·php
西柚不烦恼12 小时前
jenkins版本和java版本的依赖
java·开发语言·jenkins
stellanke12 小时前
工业化C++:3. 继承、虚函数与运行时多态:从水果加工到日志策略设计
开发语言·c++
Matlab程序猿12 小时前
【MATLAB源码-第460期】基于matlab的OTFS通信系统SLM峰均比抑制及非线性功率放大器失真与误码性能仿真
开发语言·算法·matlab·slm·otfs
kimnoic12 小时前
Python变量与数据类型全解析(最新整理)
开发语言·python
豆角焖肉13 小时前
SpringBoot+JWT 实现前后端分离登录鉴权
java·开发语言·spring boot·jwt·前后端分离·登录认证
Python图像识别13 小时前
46-【2027毕设】YOLO11田间杂草检测识别系统 - Python完整源码+PyQt5界面+训练模型+数据集
python·qt·课程设计
Wang's Blog13 小时前
Java 项目部署之 Docker工具快速入门: 自定义镜像的分层结构与写时复制
java·开发语言·docker
kimnoic21 小时前
Python数据库学习心得:SQLite、MySQL、PostgreSQL优缺点
开发语言·python
必须得开心呀1 天前
python编写带界面的软件,需要安装哪些插件
开发语言·python