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()QMetaCallEventpostEvent() 和平台事件分发器,并能根据线程、时序、生命周期和吞吐量选择正确方案。

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 本身可复制,内部指针也可能在槽执行前失效。跨线程消息应优先使用:

  • QByteArrayQStringQImage 等具有清晰值语义的类型。
  • 自定义拥有型值类型。
  • 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::ConnectiondoActivate()queued_activate()QQueuedMetaCallEvent 等属于源码实现细节。
  • 私有类名、字段和容器结构可能随版本变化。
  • 阅读源码的目的,是理解因果链和排障,不是在业务代码中调用私有 API。

5.1 一条连接究竟是什么

代码:

cpp 复制代码
connect(
    sender,
    &Sender::valueChanged,
    receiver,
    &Receiver::setValue,
    Qt::AutoConnection);

不是把 senderreceiver 神秘地绑在一起,而是创建一条连接记录。

概念结构:

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. 处理 UniqueConnectionSingleShotConnection 标志。
  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 emitQMetaObject::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)

AC 相同优先级,保持 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 这个模式为什么成立

  • ControllerQThread 对象生活在控制线程。
  • 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 线程规则

QWidgetQWindow 及绝大多数 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()

问:sendEventpostEvent 的区别?

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. 参考资料

相关推荐
qydz112 小时前
杰理开发板做TWS耳机类型方案分享(1)
开发语言·pcb工艺·嵌入式开发·杰理科技
Cloud_Shy6183 小时前
解读《Effective Python 3rd Edition》:从练气到老魔(第六章 Item 40 - 43)
android·开发语言·人工智能·笔记·python·学习方法
半只小闲鱼3 小时前
配置计划模块通用办公设备家具批复数合计计算
开发语言·python
qq_422152573 小时前
Word 文件太大怎么压缩?2026 年文档瘦身方案对比
开发语言·c#·word
charliedev3 小时前
Jedi:Python 自动补全与静态分析的实用工具
开发语言·python·其他
ji198594433 小时前
MATLAB 求散点曲线斜率
开发语言·算法·matlab
kaikaile19953 小时前
MATLAB 实现:Koch & Zhao 图像水印算法(DCT域)
开发语言·算法·matlab
love_muming3 小时前
链表每日一练
java·开发语言·数据结构·链表·idea·每日一练
weixin_446260853 小时前
LLM智能体在社交模拟中的决策行为分析:有限状态与LLM-based策略对比研究
开发语言·php
牛肉在哪里4 小时前
ros2 从零开始28 监听广播C++
开发语言·c++·算法·机器人