输入关键词开始搜索

Qt 多线程开发 — QThread + moveToThread

Qt 多线程开发 — 基于 QThread + moveToThread

基于 PulseQt T013(Worker+QThread 三线程改造)实战总结。


一、为什么需要多线程

单线程(T012 之前):
  主线程: UI绘制 → TCP收数据 → 协议解码 → 写DB → UI刷新
           ↑                          ↑
      用户操作流畅                  DB写入(1-5ms)卡UI

多线程(T013):
  通信线程: TCP收数据                 ← 只做IO
     ↓ QueuedConnection
  解析线程: 解码 → 写DB               ← 只做计算+磁盘
     ↓ QueuedConnection
  主线程:   UI绘制 + 用户交互          ← 永不阻塞

核心原则:哪个活会阻塞,就把它踢出主线程


二、三种线程方案对比

方案复杂度适用场景
QThread::run() 重写一次性后台任务(下载文件)
moveToThread(PulseQt 采用)长期存在的 Worker,通过信号槽通信
QtConcurrent::run纯函数、无状态的计算

三、moveToThread 核心模式

固定套路(四步)

// ① 创建 QThread(主线程)
QThread *thread = new QThread;

// ② 创建 Worker(主线程)
//    ⚠️ 构造函数不 new QObject 子对象(QSerialPort 等)
Worker *worker = new Worker;   // 无 parent

// ③ 移入线程
worker->moveToThread(thread);

// ④ 启动
thread->start();

为什么 Worker 构造时不能 new QSerialPort

// ❌ 错误 — 构造在主线程,m_serial 归属主线程
Worker::Worker() {
    m_serial = new QSerialPort;   // 线程亲和性 = 主线程
}
worker->moveToThread(thread);
// m_serial 还在主线程!后面在通信线程里操作它 → 报错

// ✅ 正确 — open 在通信线程里执行,m_serial 归属通信线程
void Worker::open() {            // 这个槽在通信线程里执行
    m_serial = new QSerialPort;  // 线程亲和性 = 通信线程
}

规则:QObject 的子对象必须在其工作线程中创建。方法:把创建延迟到第一个槽中执行。


四、跨线程通信 — QueuedConnection

为什么必须用

// AutoConnection(默认): 同线程=直接调,跨线程=排队调
connect(worker, &Worker::rawData, parser, &Parser::onData);  // 可以

// 显式指定 QueuedConnection(推荐,意图清晰)
connect(worker, &Worker::rawData, parser, &Parser::onData,
        Qt::QueuedConnection);

信号参数怎么传递

// 跨线程 signal → slot 时,参数自动深拷贝到接收线程
// 支持的类型:基础类型、QString、QByteArray、QVector 等 Qt 类型
// ⚠️ 不支持:裸指针、非 Qt 类型的引用

invokeMethod — 手动触发跨线程调用

// 让 Worker 在它自己的线程里执行 open()
QMetaObject::invokeMethod(worker, "open", Qt::QueuedConnection);

// 带参数
QMetaObject::invokeMethod(worker, "setName", Qt::QueuedConnection,
                          Q_ARG(QString, "hello"));
invokeMethod vs connect+emit场景
invokeMethod一次性调用(“去开一下串口”)
connect+emit持续事件流(“每次收到数据就通知我”)

五、线程生命周期

安全退出四步(顺序不能错)

void cleanup()
{
    // ① 关闭硬件(在 Worker 自己的线程里关)
    QMetaObject::invokeMethod(worker, "close", Qt::QueuedConnection);

    // ② Worker 在自己的线程里自杀
    worker->deleteLater();

    // ③ 退出事件循环
    thread->quit();

    // ④ 等线程结束
    thread->wait();   // 阻塞,等到线程真正退出

    worker = nullptr;
    delete thread;
}

为什么不用 terminate()

thread->terminate();  // ❌ 暴力杀线程 — 锁没释放、内存没回收、DB 没 flush
thread->quit();       // ✅ 优雅退出 — 等事件循环自然结束

窗口关闭时的处理

void MainWindow::closeEvent(QCloseEvent *event)
{
    onDisconnect();    // 先清理线程
    event->accept();   // 再关窗口
}

不写 closeEvent = 窗口先析构 → QThread 还在跑 → 报 “Destroyed while still running”。


六、QMutex — 数据共享

parseWorker 和 UI 线程都访问同一个 DataBuffer:

// ParseWorker(解析线程)
void onFrame(const Frame &f) {
    m_buffer.push(dp);   // 加锁写入
}

// RealTimeChart(UI 线程)
void paintEvent() {
    auto snap = m_buffer->snapshot();  // 加锁拷贝
}

DataBuffer 内部的线程安全保证

// push() — 可能被解析线程调用
void DataBuffer::push(const DataPoint &dp) {
    QMutexLocker lock(&m_mutex);   // ① 加锁
    m_ring[m_head] = dp;
    m_head = (m_head + 1) % m_maxSize;
    emit bufferUpdated(1);          // ② 发信号
}                                    // ③ 锁释放

// snapshot() — 可能被 UI 线程调用
QVector<DataPoint> snapshot() const {
    QMutexLocker lock(&m_mutex);   // 加锁 → 拷贝 → 解锁 → 返回副本
}

死锁预防

// ⚠️ 同线程 + signal/slot = 可能死锁
connect(&m_buffer, &DataBuffer::bufferUpdated,
        this, &DataTableModel::onUpdate,
        Qt::QueuedConnection);   // ← 必须 QueuedConnection!

同线程下 DirectConnection = 当场调用槽 → 槽里又调 snapshot() → 同一把锁再加 → 死锁。用 QueuedConnection → 锁释放后事件循环再调槽。


七、moveToThread 常见坑

现象原因规则
Cannot move to target threadWorker 有 parentmoveToThread 前不能设 parent
SendEvent 断言失败主线程直接调了 worker->close()必须用 invokeMethod
Destroyed while running窗口关了线程还在跑closeEvent 清理
Worker 槽不执行线程没 start() 或 Worker 没 moveToThread检查顺序
信号收不到跨线程但没用 QueuedConnection显式加第五个参数
数据竞争两个线程同时写同一变量加 QMutex 或用信号槽传值

八、PulseQt 线程架构

┌─────────────────────────────────────────┐
│                 主线程                    │
│  MainWindow                              │
│  ├── QTableView ← DataTableModel         │
│  ├── RealTimeChart ← 25FPS 定时器        │
│  └── snapshot() → DataBuffer (QMutex)    │
│        ↑ QueuedConnection                │
├─────────────────────────────────────────┤
│               解析线程                    │
│  ParseWorker                             │
│  ├── ProtocolDecoder (状态机)            │
│  ├── DataBuffer (环形缓冲, QMutex)      │
│  └── DatabaseManager (SQLite WAL)       │
│        ↑ QueuedConnection                │
├─────────────────────────────────────────┤
│               通信线程                    │
│  TcpWorker / SerialWorker               │
│  ├── QTcpSocket / QSerialPort           │
│  └── readyRead → rawDataReceived        │
└─────────────────────────────────────────┘
线程做什么不做什么
主线程UI 绘制、用户交互不碰 IO、不做解码
解析线程解码+DB写入+缓冲不碰 UI
通信线程收发字节不关心数据含义

九、什么时候用多线程

场景是否需要方案
数据库写入踢到解析线程
TCP/串口收发通信线程
文件读写worker 线程
复杂计算(>50ms)worker 线程
UI 绘制只能在主线程
简单赋值/标记主线程就行,加线程反而复杂

判断标准:这个操作会阻塞超过一帧(16ms)吗?会 → 开线程。不会 → 主线程。