# Qt 多线程编程 —— 一课时基准讲义

> **适用对象**：已掌握 C++ 基础语法与 Qt 信号槽机制的中初级学员。
> **讲解时长**：约 60 分钟（含看代码与提问），各节标题后标注的建议时长可作节奏参考。
> **配套环境**：Qt 5（本文示例基于 Qt 5.14 验证；worker-object 等核心模式在 Qt 6 完全一致）。
> **核心目标**：学完后能回答三个问题——① Qt 为什么需要多线程；② 槽函数到底在哪个线程执行；③ 给你一段耗时逻辑，你能用最稳妥的方式把它移出主线程而不崩溃。

---

## 引言：为什么要多线程（5′）

绝大多数 GUI 程序都只有**一个主线程**（也叫 GUI 线程）。Qt 的主线程一直在跑一个
**事件循环**（`QApplication::exec()`）——它不停地从事件队列里取事件并处理：鼠标点击、
键盘按键、定时器到期、重绘请求、网络数据到达……

**关键事实**：事件循环是**串行**的。处理一个事件时，后面的事件只能排队等。

那么，当主线程在执行一个**耗时操作**（比如解析一个大文件、做一次复杂计算、等待网络响应）时，
会发生什么？——它在忙，事件循环停摆，所有排队的事件（包括"请重绘界面""请响应鼠标"）
全部被卡住。表现就是：

- 按钮点了没反应；
- 窗口拖不动、缩放不了；
- 进度条不动、界面"未响应"、Windows 标题栏甚至会出现"程序无响应"。

这就是 Qt 多线程存在的**唯一根本理由**：**把耗时操作搬出主线程，让主线程专心响应界面。**

> 一句话心法：**多线程在 Qt 里的首要目的不是"更快"，而是"界面不卡"。** 当然，顺带还能用
> 多核并行加速，但那是第二位的。记住这个优先级，后面所有设计决策都有了依据。

---

## 一、基本概念（12′）

### 1.1 线程与进程

一句话带过：**进程是资源分配单位，线程是 CPU 调度单位**。一个进程至少有一个线程（主线程），
可以再开若干子线程；同进程的线程**共享同一片内存空间**（这点对后面的"竞态/加锁"至关重要）。

### 1.2 主线程与事件循环

如引言所述，主线程跑 `exec()` 事件循环。**Qt 里只有主线程能创建和操作 GUI 控件**
（QWidget 及其子类）——这是铁律，记住它，"最佳实践"里会反复用到。

### 1.3 线程亲和性（thread affinity）—— 本课最重要的概念

每个 `QObject` 都有一个"**归属线程**"，即它的**线程亲和性**。可以用 `obj->thread()` 查询。

亲和性的核心规则：**一个 QObject 的槽函数，默认在它「自己归属的线程」里执行，而不是在
调用它的线程里执行。** 这句话是理解 Qt 多线程的钥匙。

默认情况下，一个对象在哪条线程被 `new` 出来，它的亲和性就是哪条线程。要改变亲和性，
用 `moveToThread(thread)`——这一步之后，该对象的槽函数就会在 `thread` 里执行。

> 把亲和性理解成"这个对象的家"。信号槽机制会自动把"去这个对象家敲门"（调用它的槽）
> 这件事，调度到它家所在的线程去做——这就是下一小节"排队连接"的本质。

### 1.4 跨线程信号槽：三种连接类型

`QObject::connect` 在建立连接时，会根据**发送者和接收者是否在同一线程**，自动选择连接类型：

| 连接类型 | 触发方式 | 何时被 Qt 自动选用 |
|---|---|---|
| **DirectConnection**（直接） | `emit` 处直接调用槽，**同步**，在发送者线程执行 | 发送者与接收者**在同一线程** |
| **QueuedConnection**（排队） | `emit` 把调用**投递**到接收者线程的事件队列，**异步**，在接收者线程执行 | 发送者与接收者**在不同线程** |
| **AutoConnection**（自动，默认） | 运行时判断二者是否同线程，同则直接、不同则排队 | 不指定类型时的默认值 |

**排队连接是 Qt 多线程的魔法所在**：你在子线程里 `emit resultReady(x)`，Qt 会自动把
"调用主线程那个槽"打包成一个事件，投递到主线程事件队列。主线程在下一轮事件循环里取出它、
执行槽。于是——**子线程算出的结果，自动、安全地"回到"了主线程，全程不需要你手动加锁。**

这也是为什么官方推荐"用信号槽在跨线程间传递数据，而不是共享变量 + 锁"。

---

## 二、使用线程的方法（15′）

Qt 提供了几条不同的多线程路径，从底层到高层依次为：

### 方法一：继承 `QThread`，重写 `run()`

最古老、教程里最常见的写法。把耗时逻辑写进 `run()`，`start()` 后 `run()` 在子线程执行。

```cpp
class MyThread : public QThread {
protected:
    void run() override {
        // 这段代码在子线程执行
        // ... 耗时操作 ...
    }
};
```

**能跑，但容易踩坑**：很多人以为"MyThread 的所有槽都在子线程执行"——**错**。`QThread`
对象本身（包括它的槽）依然活在**创建它的线程**（通常是主线程）。只有 `run()` 里的代码
在子线程。这个认知错位会导致"明明 moveTo 了，槽却还在主线程跑"的困惑。

### 方法二：Worker-Object + `moveToThread`（官方推荐）⭐

不继承 `QThread`，而是把耗时逻辑写成一个**普通 `QObject` 子类**（Worker）的槽函数，
再用 `moveToThread()` 把它整个挪到子线程。此后 Worker 的槽就在子线程执行，
通过信号槽与主线程双向通信。

这是 Qt 官方**强烈推荐**的写法，第三节会给完整示例。优点：槽确实在子线程、用信号槽通信
天然线程安全、代码清晰。

### 方法三：`QRunnable` + `QThreadPool`（任务并行）

适合"**一堆独立的、可并行的小任务**"（如分块计算、批量处理）。每个任务继承 `QRunnable`，
`run()` 写计算逻辑，丢进 `QThreadPool` 由它调度到若干线程并行执行。

与方法二相比：worker-object 适合"**一个长期存在的后台对象**"，`QRunnable` 适合
"**大量一次性任务**"。第三节也给出示例。

### 方法四（了解即可）：`QtConcurrent` + `QFuture`

更高层、更省代码的 API。`QtConcurrent::run()` 一行就能把一个函数扔到线程池；
`QFuture` 用来取结果/取消/等待。**适合简单的一次性并行**，缺点是控制粒度粗、调试不如
QThread 直观。本课不展开，知道"它是更上层的封装"即可。

### 决策表：何时用哪种？

| 场景 | 推荐方法 |
|---|---|
| 一个长期运行的后台服务（持续监听、定期处理） | **方法二**（worker-object） |
| 大量独立、可并行的一次性任务（批量、分块） | **方法三**（QRunnable + 线程池） |
| 简单地把单个函数异步跑一次，不想写类 | **方法四**（QtConcurrent::run） |
| 维护老代码、或确需完全控制线程入口 | 方法一（继承 QThread，慎用） |

> 给中初级学员的默认建议：**绝大多数情况用方法二（worker-object）。** 它最不易出错、最契合
> Qt 的信号槽体系。除非任务特征明确指向方法三/四，否则一律用方法二起步。

---

## 三、如何使用多线程（18′）

### 3.1 完整示例：worker-object 模式（主线示例，务必看懂）

下面是一个**自包含、可编译、可运行**的控制台程序。它把"1 加到 5000 万"这件耗时计算
丢到子线程，算完把结果安全地传回主线程打印。

```cpp
// main.cpp —— worker-object 多线程最小完整示例（控制台，无需 GUI）
#include <QCoreApplication>
#include <QObject>
#include <QThread>
#include <QDebug>

// 工作对象：普通 QObject 子类。注意它「不」继承 QThread。
class Worker : public QObject {
    Q_OBJECT
public slots:
    void doWork() {                       // 这个槽将运行在子线程（因为 worker 被 moveToThread）
        // 模拟耗时计算（真实场景：解析大文件、网络请求、复杂运算……）
        // 用 qint64 防止溢出：5000 万求和 ≈ 1.25e15，远超 int 上限
        qint64 sum = 0;
        for (int i = 1; i <= 50'000'000; ++i) sum += i;
        emit resultReady(sum);            // 算完发信号 → 跨线程排队连接，自动带回主线程
        emit finished();                  // 通知：工作完成
    }
signals:
    void resultReady(qint64 result);      // 把结果交给主线程
    void finished();                      // 通知主线程：可以结束了
};

int main(int argc, char* argv[]) {
    QCoreApplication app(argc, argv);

    auto* thread = new QThread;           // 线程对象（本身活在主线程，管理一个子线程）
    auto* worker = new Worker;            // worker 当前还在主线程
    worker->moveToThread(thread);         // ★ 关键一步：把 worker 挪到子线程

    // —— 四条核心接线（全部用函数指针语法，连接类型交由 Qt 自动判定）——
    // ① 线程一启动 → 立刻开始干活
    QObject::connect(thread, &QThread::started,  worker, &Worker::doWork);

    // ② 结果回主线程：接收者 &app 在主线程，于是这是「排队连接」，
    //    回调 lambda 必然在主线程执行（看下一行的断言验证）
    QObject::connect(worker, &Worker::resultReady, &app, [](qint64 result) {
        qDebug() << "计算结果：" << result
                 << "（回调在"
                 << (QThread::currentThread() == qApp->thread() ? "主" : "子")
                 << "线程执行）";
    });

    // ③ 优雅退出 + 清理（Qt 官方标准四件套，照抄即可）
    QObject::connect(worker, &Worker::resultReady, thread, &QThread::quit);          // 算完让子线程事件循环退出
    QObject::connect(thread, &QThread::finished,   worker, &QObject::deleteLater);  // 子线程结束 → 删 worker
    QObject::connect(thread, &QThread::finished,   thread, &QObject::deleteLater);  //            → 删 thread
    QObject::connect(thread, &QThread::finished,   &app,   &QCoreApplication::quit);//            → 退出程序

    thread->start();                      // 启动子线程事件循环
    return app.exec();
}

#include "main.moc"   // Worker 定义在 .cpp 内时需要此行（拆成 .h/.cpp 则由 AUTOMOC 自动处理）
```

**走读这个示例（这是本课重点）**：

1. **Worker 不继承 QThread**——这是方法二的精髓。耗时逻辑在 `doWork()` 槽里。
2. **`moveToThread` 改变亲和性**：之后 `doWork` 在子线程执行。对比"方法一"的陷阱——
   那里只有 `run()` 在子线程，槽不在；这里 `doWork` 这个槽**确确实实在子线程**。
3. **`started → doWork`**：线程的 `exec()` 一跑起来，`started` 信号触发 `doWork`，开始干活。
4. **`resultReady → 主线程 lambda`**：因为接收者在主线程，Qt 自动用**排队连接**，
   把"调用 lambda"投递回主线程。打印里的断言会显示"主线程"——亲手验证排队连接真的把回调带回来了。
5. **优雅退出**：`resultReady → quit()` 让子线程事件循环退出 → `finished` 触发 →
   `deleteLater` 清理 worker 和 thread，最后 `app.quit()` 结束程序。**没有内存泄漏，没有悬空。**

> **如何亲手验证"排队连接"？** 把第 4 条的回调想象成"更新一个 QLabel"。因为回调确定在主线程，
> 所以可以**安全地**操作 GUI 控件——这正是 worker 模式能安全配合 GUI 的根本原因。

### 3.2 完整示例：`QRunnable` + `QThreadPool`（任务并行）

同样是"1 加到 5000 万"，这次切成 4 段丢给线程池并行算，4 段结果都回到主线程汇总。

```cpp
// main.cpp —— QRunnable + 线程池：任务并行最小示例
#include <QCoreApplication>
#include <QObject>
#include <QRunnable>
#include <QThreadPool>
#include <QDebug>

// 一个可并行执行的「任务」。QRunnable 本身不发信号，要发结果须同时继承 QObject。
class SumTask : public QObject, public QRunnable {
    Q_OBJECT
    int from_, to_;
public:
    SumTask(int from, int to) : from_(from), to_(to) { setAutoDelete(true); }
    void run() override {                 // run() 在线程池的某个工作线程执行
        qint64 sum = 0;
        for (int i = from_; i < to_; ++i) sum += i;
        emit done(sum);                   // 跨线程排队连接，把结果带回主线程
    }
signals:
    void done(qint64 sum);
};

int main(int argc, char* argv[]) {
    QCoreApplication app(argc, argv);

    const int N = 4;                      // 切 4 段并行
    const int total = 20'000'000;
    const int step = total / N;
    auto* results = new qint64[N]{};      // 存每段结果
    int got = 0;                          // 已返回的段数

    for (int i = 0; i < N; ++i) {
        auto* task = new SumTask(i * step, (i + 1) * step);
        // 接收者 lambda 在主线程 → done 经排队连接带回主线程，回调串行执行，天然无锁
        QObject::connect(task, &SumTask::done, &app, [&, i](qint64 sum) {
            results[i] = sum;
            if (++got == N) {             // 4 段全部返回
                qDebug() << "总和：" << results[0] + results[1] + results[2] + results[3];
                app.quit();
            }
        });
        QThreadPool::globalInstance()->start(task);   // 丢进全局线程池
    }

    return app.exec();
    // 注：results 是堆对象，示例为简洁未 delete；真实程序用智能指针或栈上容器管理
}

#include "main.moc"
```

**两个示例对照，记住要点**：

- **示例 3.1**（worker-object）：一个 Worker 长期住在一个子线程里，适合"后台服务"。
- **示例 3.2**（QRunnable）：多个一次性任务被线程池调度，适合"批量并行"。
- **两者都靠排队连接把结果安全带回主线程**——这就是贯穿所有 Qt 多线程的统一范式：
  **子线程只管算，结果用信号发给主线程，主线程负责一切后续（更新界面/汇总/落盘）。**

---

## 四、最佳实践与常见坑（8′）

每条都对应一个**真实会崩或会卡**的坑，逐条记住可省去项目期大量排查时间。

### ① 绝不在工作线程里访问 GUI 控件

`QWidget` 及其子类**不是线程安全**的，且只能由主线程操作。在工作线程里直接调
`label->setText(...)` 是**未定义行为**——可能崩溃、可能乱码、可能偶发花屏。

**正解**：子线程算出数据后 `emit` 一个信号，由主线程的槽（排队连接）去更新控件。
（回顾示例 3.1 的回调——它确认在主线程，所以那里才是碰 GUI 的合法位置。）

### ② 优先用信号槽传数据，退而求其次才用 `QMutex`

子线程之间、子线程与主线程之间，**优先选择"通过信号槽传递数据的所有权"**（一个线程
算完、发出去、就不再碰它），而不是"多个线程共享一块内存 + 加锁"。

只有当确实**无法避免共享同一块可变数据**时（如多个写线程更新同一个计数器），才用
`QMutex`（互斥锁）/ `QReadWriteLock`。锁是最后的手段，不是首选——它带来死锁、
性能下降、调试困难。

### ③ `moveToThread` 的两个时序要点

- **`worker` 不能有 parent**。`new Worker(parent)` 之后 `moveToThread` 会失败。务必
  `new Worker`（无 parent），由 `deleteLater` 负责它的生命周期。
- **先 `connect`，再 `start`**。确保线程一启动，信号槽已就绪，避免"线程已跑、接线还没好"
  的竞态。

### ④ 优雅退出：`quit()` + `wait()`，别暴力 `terminate()`

- 让线程的事件循环自然结束：`thread->quit()`（请求退出）配 `deleteLater` 清理（见示例）。
- 若是 `QThread` 作为类成员：在析构里 `quit()` + `wait()`，确保线程真正停下再销毁对象。
- **绝不轻易用 `terminate()`**：它强行杀线程，可能导致资源未释放、锁未解开、数据损坏。

### ⑤ 亲和性决定槽的执行线程（再强调一次）

`connect` 里**接收者所在线程**决定了槽在哪执行（排队连接时）。当你疑惑"这个槽为什么
在主线程跑"——去查接收者的 `thread()`，答案就在那里。这是排查 Qt 线程问题的第一招。

### ⑥ 调试利器：`QThread::currentThread()`

任何时候想确认"现在我在哪个线程"，打印 `QThread::currentThread()` 或与 `qApp->thread()`
比较。示例 3.1 就用它验证了回调确实在主线程。**排查多线程问题的第一行代码就是它。**

### 常见坑速览表

| 现象 | 根因 | 对应条目 |
|---|---|---|
| 界面卡死、"未响应" | 耗时操作在主线程 | 引言 / ① |
| 工作线程更新控件崩溃 | 子线程碰了 GUI | ① |
| moveToThread 后槽还在主线程跑 | 误用继承 QThread（方法一陷阱） | 二·方法一 / ⑤ |
| 程序退出时崩溃/卡死 | 线程未优雅退出 | ④ |
| 偶发数据错乱 | 多线程共享可变数据未加锁 | ② |

---

## 小结：一页速查表（2′）

**Qt 多线程的三条心法**：

1. **动机**：多线程的首要目的是**界面不卡**（把耗时操作搬出主线程），其次是多核加速。
2. **核心抽象**：**线程亲和性**决定槽在哪个线程跑；**排队连接**自动把跨线程调用安全调度到目标线程。
3. **统一范式**：**子线程只算，结果用信号发回主线程，主线程负责一切后续（含更新 GUI）。**

**默认选型**：长期后台服务 → worker-object（方法二）；批量并行任务 → QRunnable + 线程池（方法三）。

**四条铁律**：① 子线程不碰 GUI；② 优先信号槽传数据、慎用锁；③ moveToThread 先 connect 再 start、worker 无 parent；④ 优雅 quit+wait、禁用 terminate。

---

## 附：构建运行（一行说明）

上述示例为单 `.cpp` 控台程序。最小 CMake 片段：

```cmake
find_package(Qt5 COMPONENTS Core REQUIRED)
add_executable(thread_demo main.cpp)
target_link_libraries(thread_demo PRIVATE Qt5::Core)
set_target_properties(thread_demo PROPERTIES AUTOMOC ON)   # 自动处理 moc，无需手写 main.moc
```

开启 `AUTOMOC` 后，示例末尾的 `#include "main.moc"` 可去掉，CMake 会自动生成元对象代码。
若把 Worker/SumTask 拆到独立的 `.h`，则无论哪种构建方式都无需手动处理 moc。

> **运行提示（看不到 qDebug 输出？）**：在 Windows + 官方**发行版（release）** Qt 下，
> `qDebug` 默认走系统调试输出（`OutputDebugString`）而非终端，控制台程序可能看不到打印。
> 这**不是程序没执行**——线程照常运行、结果照常计算。验证手段任选其一：① 用 Debug 版 Qt；
> ② 临时改用 `std::cerr << ... << std::flush;` 打印关键路径；③ 在 IDE/调试器输出窗口查看。
> 示例逻辑的正确性不依赖此输出（上述三种示例的运行行为均已在 Qt 5.14.2 上端到端验证通过）。
