Qt 多线程

QThread类提供了一种独立于平台的方法来管理线程 。 QThread对象在程序中管理一个控制线程。 QThreads在 run() 中开始执行。默认情况下,run()通过调用 exec()来启动事件循环, 并在线程中运行 Qt事件循环。您可以通过使用QObject::moveToThread()将 worker对象移动到线程来使用它们。

下面详细介绍 Qt 中的 QThread,包括其原理、常用用法、两种工作模式、信号槽机制、常见坑点与最佳实践。


1. QThread 是什么?

QThread 是 Qt 中的线程类,用来在应用程序中创建和控制线程,用于执行耗时任务,从而避免主线程(UI 线程)阻塞。

Qt 的线程具有如下特点:

  • 每个 QThread 代表一个操作系统线程

  • QObject 的事件循环与线程绑定(非 QObject 的行为则不受 Qt 控制)

  • 线程之间可以安全使用信号-槽通信(自动跨线程调用)


2. QThread 的两种使用方式

Qt 官方推荐方式 2:对象移入线程


方式 1:继承 QThread 并重写 run() 方法

你把任务写进 run(),线程启动后自动执行。

示例

cpp 复制代码
class MyThread : public QThread {
    Q_OBJECT
public:
    void run() override {
        // 耗时任务
        for (int i = 0; i < 5; ++i) {
            QThread::sleep(1);
            qDebug() << "Running in thread:" << QThread::currentThread();
        }
    }
};

使用:

cpp 复制代码
MyThread *t = new MyThread();
t->start();

特点

✔ 简单直观

run() 是线程主函数

不能使用 QTimer、信号槽等事件循环(除非开启 exec())

❌ 不适合复杂对象结构

❌ Qt 官方不推荐


方式 2(推荐):使用工作对象 + moveToThread()

保持 QThread 只表示"线程",逻辑由普通 QObject 执行。

示例:正确的做法

cpp 复制代码
class Worker : public QObject {
    Q_OBJECT
public slots:
    void doWork() {
        for (int i = 0; i < 5; ++i) {
            QThread::sleep(1);
            emit progress(i);
        }
    }

signals:
    void progress(int value);
};
cpp 复制代码
QThread *thread = new QThread();
Worker *worker = new Worker();

worker->moveToThread(thread);

QObject::connect(thread, &QThread::started, worker, &Worker::doWork);
QObject::connect(worker, &Worker::progress, [](int v){
    qDebug() << "progress:" << v;
});
QObject::connect(worker, &Worker::progress, thread, &QThread::quit); // 完成后退出线程

thread->start();

特点

✔ 使用 Qt 事件循环

✔ 支持定时器、信号槽

✔ 推荐方式,最安全、最灵活

✔ 易于控制生命周期


3. QThread 的信号与槽 ------ 自动跨线程

Qt 的信号槽机制自动识别跨线程情况:

  • 若 sender 和 receiver 在不同线程

    → 自动使用 QueuedConnection(队列连接)

    → 函数在 receiver 所在线程执行

  • 若在同一个线程

    → 使用直接调用(Direct)

因此 QThread 非常适合线程间通信。


4. QThread 中的事件循环

线程事件循环由 exec() 启动:

在方式 2 中,默认 QThread 会自动调用 exec()(因为内部 run() 调用 exec())。

如果你选择方式 1 自己重写 run():

cpp 复制代码
void run() override {
    exec();   // 必须显式调用,才能使用事件循环(Timers、Signals)
}

没有事件循环则:

  • QTimer 不工作

  • 跨线程信号槽不会执行


5. QThread 的常见错误

错误:在子线程直接操作 UI

cpp 复制代码
label->setText("abc"); // 错误!UI 只能在主线程处理

错误:delete QThread 后工作对象还在线程中

必须确保线程退出,然后再 delete。


6. 正确关闭 QThread

cpp 复制代码
thread->quit(); // 发出事件循环退出信号
thread->wait(); // 阻塞等待线程结束

不要直接调用 terminate()(会强制终止线程,导致资源泄露)。


7. 使用 QThreadPool / QRunnable 代替 QThread

对于大量短任务,推荐:

  • QThreadPool

  • QtConcurrent::run()

它们自动管理线程池,更高效。


⭐ 8. 总结

特性 继承 QThread moveToThread(推荐)
支持事件循环 ❌ 默认不支持 ✔ 支持
支持信号槽 较差 ✔ 完整支持
易维护性 较差 ✔ 最佳
官方推荐 ❌ 不推荐 ✔ 推荐

/**********************************************************************************************************/

讲解Qt 提供的线程同步类QMutexQMutexLockerQReadLockerQWriteLocker,并分析它们的使用场景和区别。


1️⃣ QMutex(互斥锁)

作用

QMutex 用于保护共享资源,确保同一时刻只有一个线程访问该资源。它是 最基础的互斥锁

常用方法

cpp 复制代码
QMutex mutex;

// 加锁
mutex.lock();

// 解锁
mutex.unlock();

// 尝试加锁,不阻塞
if (mutex.tryLock()) {
    // 成功加锁
    mutex.unlock();
}

特点

  • 如果一个线程已加锁,其他线程会阻塞,直到锁被释放

  • 可以防止数据竞争(race condition)

示例

cpp 复制代码
QMutex mutex;
int counter = 0;

void increment() {
    mutex.lock();
    counter++;
    mutex.unlock();
}

2️⃣ QMutexLocker(互斥锁的智能封装)

作用

QMutexLocker 是 RAII 风格(资源获取即初始化)的锁管理类,它能自动加锁和解锁,防止忘记 unlock 或异常导致死锁。

用法

cpp 复制代码
QMutex mutex;

void increment() {
    QMutexLocker locker(&mutex); // 自动 lock
    counter++;
} // 离开作用域自动 unlock

特点

  • 自动管理 lock/unlock

  • 避免手动调用 lock/unlock 时出错

  • 强烈推荐在函数内部局部变量使用


3️⃣ QReadWriteLock(读写锁)

在 Qt 中,读写锁由 QReadWriteLock 实现,同时支持多个读线程,但写线程是互斥的。

工作模式

  • 共享读锁(read lock):多个线程可以同时读取

  • 排他写锁(write lock):写线程独占,写时阻塞其他读/写线程


QReadLocker / QWriteLocker

它们是 QReadWriteLock 的 RAII 封装类,自动管理锁的加解锁。

QReadLocker

cpp 复制代码
QReadWriteLock lock;

void readData() {
    QReadLocker locker(&lock); // 加读锁
    // 读取共享数据
} // 自动解锁

QWriteLocker

cpp 复制代码
void writeData() {
    QWriteLocker locker(&lock); // 加写锁
    // 修改共享数据
} // 自动解锁

特点

  • 读操作不会阻塞其他读线程

  • 写操作阻塞所有读写线程

  • RAII 风格,安全可靠


4️⃣ QMutex / QReadWriteLock 的区别

特性 QMutex QReadWriteLock
允许同时读线程 ❌ 不允许 ✔ 允许
写线程互斥
性能 简单,锁/解锁开销低 适合读多写少场景,读效率高
使用场景 小规模互斥,临界区短 共享资源读多写少,读操作频繁

5️⃣ 使用建议

  1. 简单互斥 → 用 QMutexQMutexLocker

  2. 读多写少 → 用 QReadWriteLock + QReadLocker/QWriteLocker

  3. RAII 风格优先 → 使用 Locker,避免忘记 unlock

  4. 不要在 UI 线程长时间加锁 → 防止界面卡死


6️⃣ 综合示例

cpp 复制代码
QReadWriteLock lock;
int sharedData = 0;

void reader() {
    QReadLocker locker(&lock); // 加读锁
    qDebug() << "Read data:" << sharedData;
}

void writer() {
    QWriteLocker locker(&lock); // 加写锁
    sharedData++;
}
  • 多个 reader 可以同时执行

  • writer 执行时,reader 和其他 writer 都会阻塞


总结:

  • QMutex + QMutexLocker → 简单互斥,保护临界区

  • QReadWriteLock + QReadLocker / QWriteLocker → 适合读多写少的共享资源

  • Locker 类优先 → 自动解锁,安全可靠

一、继承QThread 的线程 例程

cpp 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QThread>
#include <QDebug>
#include <QPushButton>

class WorkerThread;     //声明类

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);  //构造函数,设置父控件
    ~MainWindow();

private:
    WorkerThread *workerThread;     //创建对象
    QPushButton *pushButton;        //使用此按钮点击后开启线程

private slots:
    void handleResults(const QString &result);  //用于接收线程发送的信号
    void pushButtonClicked();                   //点击按钮开启线程
};
//创建一个WorkerThread 类继承于 QThread
class WorkerThread : public QThread {

    Q_OBJECT        //用到信号槽即需要此宏定义

public:
    WorkerThread(QWidget *parent = nullptr) {   //构造函数,设置夫控件
        Q_UNUSED(parent);                       //避免未使用参数的警告
    }
    //重写 run 方法,继承 QThread 的类,只有 run 方法是在新的线程里面
    void run() override {
        QString result = "线程开启成功";
        //这里写比较耗时的操作
        //...
        //延时2s,把延时当耗时操作
        sleep(2);
        emit resultReady(result);   //emit关键字标记信号的发射,发射信号
    }
signals:

    void resultReady(const QString &s);  //定义信号,用于通知结果已准备好,在槽函数里使用
};

#endif // MAINWINDOW_H
cpp 复制代码
#include "mainwindow.h"
#include <QDebug>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    this->setGeometry(0,0,800,480);

    pushButton = new QPushButton(this);
    workerThread = new WorkerThread(this);

    pushButton->resize(100,40);
    pushButton->setText("开启线程");

    connect(workerThread,SIGNAL(resultReady(QString)),
            this,SLOT(handleResults(QString)));
    connect(pushButton,SIGNAL(clicked()),
            this,SLOT(pushButtonClicked()));
}

MainWindow::~MainWindow()
{
    //进程退出,注意这里run()方法没有写循环,该方法需要有循环才生效
    workerThread->quit();
    //阻塞等待2秒检查一次进程是否退出
    if(workerThread->wait(2000)) {
        qDebug()<<"线程已结束!"<<endl;
    }
}

void MainWindow::handleResults(const QString &result) {
    qDebug()<<result<<endl;     //打印线程发送过来的结果
}
void MainWindow::pushButtonClicked() {

    if(!workerThread->isRunning()){
        workerThread->start();
    }
}

这是一个非常好的、与 Qt 多线程编程 密切相关的问题。理解 QObject 在线程中的行为是使用 Qt 进行并发编程的关键。


🚀 Qt 继承 QObject 的线程 形象讲解

在 Qt 中,**线程亲和性(Thread Affinity)**是一个核心概念,它决定了一个 QObject 对象"属于"哪个线程。

🏠 形象比喻:线程与家

你可以把一个 线程 (QThread) 想象成一个 "家" ,而一个 QObject 对象 就像一个 "家庭成员"

  1. 出生地原则(Creation Thread):

    • QObject 在哪个线程中被创建,它就天然地"出生"并属于 那个线程。这是它的初始亲和性
  2. 独居与派对(Event Loop):

    • 每个线程(家)如果想要处理 信号与槽的异步连接Qt::QueuedConnection)或 定时器事件网络事件 等,它就必须有一个 事件循环(Event Loop) ,就像家里的一个"派对/待办事项处理中心"。

    • QThread 的 run() 方法 默认会启动一个事件循环。

  3. 搬家(moveToThread()):

    • QObject 对象可以通过调用 obj->moveToThread(targetThread) 来"搬家",改变它所属于的线程。

    • 限制: 如果这个"家庭成员"已经有了一个"家长"(即设置了 parent),那么它就不能再搬家了。有父对象(Parent)的 QObject 必须和它的父对象待在同一个线程中。

⚠️ 为什么不应该直接继承 QThread 来执行任务?

传统的(但不推荐的)Qt 线程用法是继承 QThread 并重写 run()。但是,QThread 对象本身 是线程的控制器/管理者 ,它通常属于创建它的线程 (比如主线程),而不是它所运行的新线程

错误示范: 如果你直接在继承 QThread 的子类中添加 QObject 成员和槽函数,你可能会在新线程 中执行任务,但这些槽函数(因为它继承自 QObject)却可能在 主线程 中被调用,造成线程安全问题!

推荐做法:Worker 模式

最安全和推荐的做法是使用 Worker 对象模式

  1. 创建 Worker: 创建一个继承自 QObject 的类 (Worker),它包含你想要在新线程中执行的任务代码。

  2. 创建 Thread: 创建一个 QThread 实例。

  3. 搬家: 调用 worker->moveToThread(thread),将 Worker 对象"搬"到新线程中。

  4. 启动: 调用 thread->start() 启动线程(开启 Worker 的新家)。

  5. 信号/槽通信: 在主线程(UI 线程)和 Worker 线程之间使用 信号(Signals)槽(Slots) 进行通信,这是 Qt 线程安全的黄金法则。因为 QObject 在搬家后,其槽函数(通过队列连接)就会在新线程的事件循环中被安全执行。


💻 C++ 示例:Worker 对象模式

我们创建一个 Worker 类来执行耗时的任务,并将其移动到一个新的 QThread 中。

1. Worker 类 (Worker.h/Worker.cpp)

这个类继承自 QObject,包含实际要执行的任务。

Worker.h

cpp 复制代码
#ifndef WORKER_H
#define WORKER_H

#include <QObject>
#include <QDebug>
#include <QThread>

class Worker : public QObject
{
    Q_OBJECT
public:
    explicit Worker(QObject *parent = nullptr);

public slots:
    // **注意:这个槽函数将在 Worker 对象所属的线程中执行**
    void doWork(const QString &parameter);

signals:
    // 任务完成时发出的信号
    void workFinished(const QString &result);
};

#endif // WORKER_H

Worker.cpp

cpp 复制代码
#include "Worker.h"

Worker::Worker(QObject *parent) : QObject(parent)
{
    // Worker 对象的初始线程亲和性是创建它的线程(通常是主线程)
    qDebug() << "Worker created in thread:" << QThread::currentThreadId();
}

void Worker::doWork(const QString &parameter)
{
    // 任务执行时,检查当前线程 ID。
    // 如果成功调用了 moveToThread,这个 ID 应该与主线程 ID 不同。
    qDebug() << "Starting work in thread:" << QThread::currentThreadId()
             << "with parameter:" << parameter;

    // 模拟耗时的计算
    long long sum = 0;
    for (int i = 0; i < 100000000; ++i) {
        sum += i;
    }

    QString result = QString("Calculation finished. Sum: %1").arg(sum);
    qDebug() << "Work finished in thread:" << QThread::currentThreadId();

    // 任务完成,通过信号通知主线程
    emit workFinished(result);
}

2. 主函数 (main.cpp)

我们将 Worker 实例移动到新创建的 QThread 实例中。

main.cpp

cpp 复制代码
#include <QCoreApplication>
#include <QThread>
#include "Worker.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    qDebug() << "Main thread ID:" << QThread::currentThreadId();
    
    // 1. 创建 QThread(新的"家")
    QThread* thread = new QThread();
    
    // 2. 创建 Worker 对象("家庭成员"),它最初在主线程中
    Worker* worker = new Worker();
    
    // 3. **核心步骤:搬家!**
    // 将 worker 对象移动到新的 QThread 实例中。
    // 此时,worker 的槽函数(如 doWork)将在 thread 线程中执行。
    worker->moveToThread(thread);

    // 4. 建立连接(线程间通信)
    
    // A. 任务启动:主线程 --> 新线程
    // 线程启动时,立即调用 worker 的 doWork 槽函数
    QObject::connect(thread, &QThread::started, worker, [worker](){
        // 注意:doWork 是一个槽函数,会在 thread 启动后,在新线程中被调用。
        worker->doWork("Heavy Task Data"); 
    });
    
    // B. 结果接收:新线程 --> 主线程
    // Worker 完成工作后,发信号通知主线程
    QObject::connect(worker, &Worker::workFinished, [](const QString& result){
        qDebug() << "Received result in thread:" << QThread::currentThreadId();
        qDebug() << "Result:" << result;
        // 收到结果后可以停止应用,或者进行UI更新等操作
        QCoreApplication::quit(); 
    });
    
    // C. 清理工作(可选,但推荐):确保线程和对象被正确删除
    QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
    QObject::connect(thread, &QThread::finished, worker, &QObject::deleteLater);


    // 5. 启动线程(开启新的"家"的事件循环)
    thread->start();

    // 运行事件循环,等待结果信号
    return a.exec();
}

🔑 总结关键点:

  • Worker 继承 QObject 使其具备信号/槽机制和线程亲和性。

  • moveToThread() 改变 Worker 对象的线程亲和性,让它的槽函数在新线程的事件循环中安全执行。

  • QThread::start() 启动新线程,并在新线程中开始事件循环,等待事件(如信号触发槽函数)。

  • 信号与槽: 在不同线程之间安全通信的桥梁。Qt 会自动使用 Qt::QueuedConnection 来保证跨线程调用的安全。

这个模式保证了耗时操作在一个独立的线程中运行,不会阻塞主线程(UI 线程),同时所有 QObject 的事件处理都发生在它所属的线程中,从而避免了数据竞争和崩溃。

/*********************************************************************************************************/

堆对象和栈对象:

new 出来的对象 → 堆 → 可以 deleteLater
普通写法声明的对象 → 栈 → 不能 deleteLater

写法 类型 内存位置 生命周期 可否 deleteLater
QThread workerThread; 栈对象 stack 自动释放 ❌ 禁止
QThread *workerThread = new QThread; 堆对象 heap 手动释放 ✔ 可以 deleteLater
QThread *workerThread;(未 new) 仅是指针 与对象无关 指针不管理对象 N/A

✅ 判断是否为堆对象的核心规则

使用 new 创建的对象 → 堆对象

QThread *t = new QThread; // 堆对象

必须手动 delete 或者用 deleteLater。


作为类成员指针,但未 new,本身不是对象

class A { QThread *t; // 只是指针,本身不是对象 };

必须配套:

t = new QThread(); // 才会创建堆对象

/*************************************************************************************************************/

槽函数介绍:

下面给你一个 最完整、最清晰、最实用的 Qt connect() 介绍,包括四种写法、语法、规则、优缺点,以及 Qt5 / Qt6 差异。

cpp 复制代码
QObject::connect(
    const QObject *sender,
    PointerToMemberFunction signal,
    const QObject *receiver,
    PointerToMemberFunction slot
);
参数序号 参数名称 类型 含义 作用
1 sender const QObject* 发送者对象 哪个对象发出了信号。通常是触发事件的控件(如按钮、滑动条等)或数据对象。
2 signal 信号函数指针 信号 哪个信号被发出。这是发送者对象的一个成员函数,在特定事件发生时被调用。
3 receiver const QObject* 接收者对象 哪个对象要响应这个信号。通常是包含槽函数(处理逻辑)的窗口或业务对象。
4 slot 槽函数指针 槽函数 执行什么操作来响应信号。这是接收者对象的一个成员函数,当信号发出时它会被自动执行。

结构如下:

  1. 什么是 connect

  2. 四种写法(宏版、函数指针版、lambda版、自动连接)

  3. 参数规则

  4. 线程行为

  5. 常见错误

  6. 最推荐用法


connect 是干什么的?

Qt 的核心机制:信号(signal)和槽(slot)

当一个对象产生事件(比如按钮点击)→ 发射 signal

另一个对象就可以响应(执行 slot)

connect() 就是绑定两者的函数。


② connect() 的四种写法

1. Qt4 老式 SIGNAL / SLOT 宏写法(常见但不推荐)

cpp 复制代码
connect(sender, SIGNAL(signalName(Type1, Type2)),
        receiver, SLOT(slotName(Type1, Type2)));

示例:

cpp 复制代码
connect(button, SIGNAL(clicked()),
        this, SLOT(onButtonClicked()));

特点:

  • 字符串匹配,不检查类型

  • 写错里面的名字不会报错

  • 运行期才发现错误 → 难调试

  • Qt5/6 中继续支持


2. Qt5/Qt6 新式 函数指针写法(最推荐)

cpp 复制代码
connect(sender, &SenderClass::signalName,
        receiver, &ReceiverClass::slotName);

示例:

cpp 复制代码
connect(button, &QPushButton::clicked,
        this, &MainWindow::onButtonClicked);

优点:

  • 编译期类型检查(写错直接编译报错)

  • 没有字符串开销

  • 自动推断参数类型

  • 支持重载信号(通过 QOverload

✔ Qt 官方推荐

✔ 最安全

✔ 最快


3. 使用 lambda 表达式的写法(非常常用)

cpp 复制代码
connect(sender, &SenderClass::signalName, this, [=](){
    // 写你想干的事
});

例子:

cpp 复制代码
connect(button, &QPushButton::clicked, this, [=](){
    qDebug() << "Button clicked!";
});

优点:

  • 非常灵活

  • 不需要写单独的槽函数

  • 可以捕获变量

缺点:

  • lambda 不会自动断开信号

  • 容易造成悬挂回调(dangling)


4. 自动连接(基于命名规则)

如果你写槽函数名为:

cpp 复制代码
void on_pushButton_clicked();

Qt 会自动连接,不需要写 connect。

**优点:**简单

**缺点:**重构后容易失效,不推荐大量使用


③ connect 的参数格式(信号槽断言)

信号槽必须参数一致:

属性 要求
参数数量 必须一致
参数类型 必须一致
参数顺序 必须一致
参数名 无要求

示例:

cpp 复制代码
signals:
    void valueChanged(int value);

slots:
    void onValueChanged(int value); // ✔
    void onValueChanged(QString s); // ❌ 类型不同

④ connect 与多线程行为(非常重要)

如果信号来自某线程,槽在另一线程中
Qt 会自动进行线程切换:

✔ Qt::AutoConnection(默认)

  • 如果 sender 和 receiver 在同一个线程 → 直接调用

  • 如果不在同一个线程 → 事件队列调用(异步)

等价于:

cpp 复制代码
Qt::QueuedConnection

这就是 Qt 的事件驱动线程模型。


⑤ connect 常见错误

错误写法 说明
SIGNAL / SLOT 名字写错 不会报错,但不会连接
SIGNAL 参数类型不一致 connect 返回 false
doSomething 写成 doSometing 编译不报错(老式写法)
connect 到不存在的槽函数 槽不执行
跨线程调用 GUI 程序崩溃

⑥ connect 的返回值

connect 返回 QMetaObject::Connection

你可以用它断开连接:

cpp 复制代码
QMetaObject::Connection c;
c = connect(...);

disconnect(c);

⑦ 推荐使用方式(最安全最现代)

推荐:函数指针 + lambda

cpp 复制代码
connect(button, &QPushButton::clicked, this, [this](){
    handleButton();
});

跨线程(Worker 中的信号)

cpp 复制代码
connect(worker, &Worker::resultReady,
        this,   &MainWindow::handleResults);

⑧ 总结(你必须记住)

✔ connect 的最常用写法

cpp 复制代码
connect(sender, &SenderClass::signalName,
        receiver, &ReceiverClass::slotName);

✔ SIGNAL/SLOT 宏仍被 Qt 支持,但不推荐

✔ lambda 版很灵活,广泛使用

✔ 跨线程自动使用队列连接,实现线程安全回调

✔ 栈对象不能 deleteLater

✔ 指针对象(new)可以 deleteLater


本小节案例:

cpp 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QThread>
#include <QDebug>
#include <QPushButton>
#include <QMutex>
#include <QMutexLocker>

class Worker;     //声明工人类

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);  //构造函数,设置父控件
    ~MainWindow();

private:
       //创建对象
    QPushButton *pushButton1;        //开启线程按钮
    QPushButton *pushButton2;       //打断线程按钮
    QThread workerThread;           //全局线程,这里创建的是栈对象
    Worker *worker;                 //工人类

private slots:
    void pushButton1Clicked();      //点击开始线程
    void pushButton2Clicked();      //点击打断线程
    void handleResults(const QString &);    //用于接收工人是否在工作的信号
signals:
    void startWork(const QString &);        //工人开始工作(做些耗时的操作)

};
//Worker类,这个嘞声明了doWorker1函数,将整个Worker类移至线程workerThread
class Worker : public QObject {

    Q_OBJECT

private:
    QMutex lock;        //互斥锁
    bool isCanRun;      //标志位
public:
//    Worker():isCanRun(false){}      //初始化

public slots:
    //耗时的工作都放在槽函数之下,工人可以有多份不同的工作,但每次只能去做一份
    void doWork1(const QString &parameter) {
        isCanRun = true;    //标志位为真
        while(1) {

            //此处 {} 的作用是 QMutexLocker 与 lock 的作用范围。获取锁后,
            //运行完成即解锁
            {
                QMutexLocker locker(&lock);
                if(!isCanRun){      //如果标志位不为真
                    break;
                }
            }
            QThread::sleep(2);
            emit resultReady(parameter + "doWorker1函数");
        }
        //doWork1运行完成,发送信号
        emit resultReady("打断doWorke1函数");
    }
    //void doWorker2()...
public:
    //打断线程(注意此方法不能放在槽函数下)
    void stopWork(){
        qDebug()<<"打断线程"<<endl;
        //获取锁后,运行完成后即解锁
        QMutexLocker locker(&lock);
        isCanRun = false;
    }
signals:
    //工人工作函数状态的信号
    void resultReady(const QString &result);
};


#endif // MAINWINDOW_H
cpp 复制代码
#include "mainwindow.h"
#include <QDebug>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    this->setGeometry(0,0,800,480);

    pushButton1 = new QPushButton(this);
    pushButton2 = new QPushButton(this);
    pushButton1->setGeometry(300,200,80,40);
    pushButton2->setGeometry(400,200,80,40);
    pushButton1->setText("开启线程");
    pushButton2->setText("打断线程");

    worker = new Worker;        //工人类实例化。继承 QObject的多线程类不能指定父对象
    worker->moveToThread(&workerThread);    //将worker类移至线程workerThread

    //线程完成销毁对象
    connect(&workerThread, SIGNAL(finished()),        //workerThread为栈对象
            worker, SLOT(deleteLater()));
    //这里不能使用deletelater,因为创建的是栈对象,只有堆对象才是手动删除
//    connect(&workerThread, SIGNAL(finished()),
//            &workerThread,SLOT(deleteLater()));
    //发送开始工作的信号,开始工作
    connect(this, SIGNAL(startWork(QString)),
            worker, SLOT(doWork1(QString)));
    //接受到worker发送过来的信号
    connect(worker, SIGNAL(resultReady(QString)),
            this, SLOT(handleResults(QString)));
    //点击按钮开始线程
    connect(pushButton1, SIGNAL(clicked()),
            this, SLOT(pushButton1Clicked()));
    //点击按钮关闭线程
    connect(pushButton2, SIGNAL(clicked()),
            this, SLOT(pushButton2Clicked()));

}

MainWindow::~MainWindow()
{
    //打断线程再退出
    worker->stopWork();
    workerThread.quit();
    //阻塞后,判断线程是否结束
    if(workerThread.wait(2000)){
        qDebug()<<"线程结束"<<endl;
    }
}

void MainWindow::pushButton1Clicked(){
    const QString str = "正在运行";
    if(!workerThread.isRunning()){
        workerThread.start();
    }
    //发送正在运行的信号,线程收到后执行后返回线程耗时函数+此字符串
    emit this->startWork(str);
}
void MainWindow::pushButton2Clicked(){
    if(workerThread.isRunning()){
        //停止耗时工作,跳出耗时工作的循环
        worker->stopWork();
    }
}

void MainWindow::handleResults(const QString &results){
    qDebug()<<"线程状态:"<<results<<endl;
}

函数执行流程、函数间关系、析构逻辑与用函数指针写 connect 的详解

下面我把你这段线程/worker 示例里的执行流程拆成若干步骤逐一讲清楚,解释每个函数/连接的作用、相互联系,为什么析构函数要那样写,最后给出现代(函数指针/lambda)写法的完整示例和替换方法。


一、总体运行脉络(高层流程概览)

  1. MainWindow 构造时

    • worker = new Worker;(在堆上创建 Worker)

    • worker->moveToThread(&workerThread);:把 worker 的 QObject 所属线程切换为 workerThread(后续其槽在 workerThread 上运行)。

    • 建立信号/槽连接:

      • MainWindow::startWork(QString)Worker::doWork1(QString)(当主线程发 startWork 时,slot 在 workerThread 执行,跨线程会变为队列调用)。

      • Worker::resultReady(QString)MainWindow::handleResults(QString)(worker 发结果回主线程,handleResults 在主线程执行)。

      • QThread::finished()worker->deleteLater()(线程结束时销毁 worker)。

    • 按钮连接到 pushButton1Clicked() / pushButton2Clicked()

  2. 点击"开启线程"按钮

    • 如果 workerThread 未运行:workerThread.start() 启动线程(并在该线程中运行事件循环)。

    • emit startWork("正在运行"):发射信号,Qt 根据线程归属决定调用方式。因为 MainWindow(sender)在主线程、worker(receiver)在 workerThread,默认是 Qt::AutoConnection → 会变成 Qt::QueuedConnection(即把调用封装成事件投递到 workerThread 的事件队列)。

  3. Worker::doWork1workerThread 执行

    • doWork1 被事件队列调度到 workerThread 上执行(槽函数在该线程上下文中运行)。

    • doWork1 中循环每 2 秒 emit resultReady(...),这会把信号发回主线程(因为 MainWindow::handleResults 在主线程)------同样是队列调用,handleResults 在主线程运行,打印日志。

  4. 点击"打断线程"按钮

    • 主线程调用 worker->stopWork()(该函数取得互斥锁,设置 isCanRun=false),doWork1 在下一轮循环检查锁后看到 isCanRun==false 就跳出循环并退出槽函数。

    • doWork1 结束后,若需要退出线程的事件循环,应由主线程调用 workerThread.quit()(退出事件循环)或由 worker 自己决定(通常由主线程调用 quit())。然后 workerThread.wait() 阻塞等待线程结束。

  5. 析构(MainWindow::~MainWindow)

    • 典型做法:先通知 worker 停止(worker->stopWork()),再 workerThread.quit(),再 workerThread.wait(...) 等待线程结束。这样可以保证线程内的槽运行结束、资源安全释放,避免在 MainWindow 或应用退出时出现悬挂/竞态/崩溃。

二、关键函数/动作之间的具体联系(谁调用谁、在哪个线程)

  • worker->moveToThread(&workerThread)

    worker 的 QObject::thread() 被设置为 workerThread。这意味着:

    • 以后对 worker 的槽(slots)进行队列调用 时,这些槽会在 workerThread 的线程上下文执行。

    • worker 的事件处理(例如 deleteLater() 或 QTimer)都在 workerThread 的事件循环中进行。

  • connect(this, SIGNAL(startWork(QString)), worker, SLOT(doWork1(QString)));

    → 当 this(MainWindow)在主线程发 startWork 时,因为 worker 不在主线程,Qt 使用 QueuedConnection(默认)把对 doWork1 的调用封装成事件,放入 workerThread 的事件队列。

  • emit resultReady(...)(在 workerThread 中)

    MainWindow::handleResults() 会在主线程执行(QueuedConnection),所以在该槽里可以安全更新 UI(打印日志、更新界面等)。

  • connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);

    → 线程结束(finished)时安排删除 worker(deleteLater 会在 worker 所在线程的事件循环中执行删除)。这是为了避免直接 delete 一个仍在其他线程中的 QObject。


三、析构函数里那些语句为啥要这样写(逐句解释)

你的析构常见写法:

cpp 复制代码
MainWindow::~MainWindow()
{
    worker->stopWork();        // 1
    workerThread.quit();       // 2
    if(workerThread.wait(2000)){ // 3
        qDebug()<<"线程结束"<<endl;
    }
}

解释:

  1. worker->stopWork()

    • 告诉 worker 停止循环 (设置 isCanRun=false),让 doWork1 能够尽快退出循环并返回。这一步是优雅结束工作的重要信号;如果不做,doWork1 可能一直阻塞,线程永远不会结束。
  2. workerThread.quit()

    • 请求线程的事件循环退出(如果线程正在运行事件循环)。如果 doWork1 正在运行且阻塞,这个 quit() 只会在事件循环空闲时生效;因此必须先让 worker 自行退出循环。
  3. workerThread.wait(2000)

    • 阻塞主线程等待最多 2000 ms 直到 workerThread 结束。目的是在 MainWindow 被销毁(或应用退出)前确保 workerThread 完全结束,避免线程访问已释放资源导致崩溃。

    • 如果 wait() 超时,你可能需要采取更强硬的手段(例如强制断言、记录日志),但通常应确保 worker 可以响应 stop 请求。

要点:顺序很重要 ------ 先通知 worker 停止,再请求线程退出/等待。否则你可能会在 worker 正在运行时就释放 MainWindow 资源,出现悬挂指针或 crash。


四、并发安全与锁的作用(为什么用 QMutex + QMutexLocker)

  • isCanRun 被多个线程访问(主线程会调用 stopWork(),worker 线程在循环里读 isCanRun),这是共享数据,引发竞态条件。

  • QMutex + QMutexLocker 可以保证对 isCanRun 的读写互斥,从而避免数据竞争(尤其是 stopWork() 里写 isCanRun=falsedoWork1 里读的竞态)。

  • 另外的更简洁/高效的写法是使用 std::atomic<bool>QAtomicInteger,这在只有 boolean 读写时更轻量,避免 mutex 开销。


五、stopWork() 为什么"不写在槽里"或"可以写为槽"?

  • 实际上 stopWork() 可以写成 public slots:,这样它也能通过信号从其他线程以队列方式调用(更线程安全)。

  • 但当前实现是主线程直接调用 worker->stopWork()(直接函数调用),这会在主线程中取得 QMutex,然后写 isCanRun=false。这是可行的,但注意:如果 stopWork() 在主线程调用并使用 QMutex,没有跨线程事件的序列问题。

  • 若你希望主线程不直接 访问 worker(更解耦),可以改成 emit requestStop() → connect(requestStop, worker, SLOT(stopWork())),这样会以队列方式在 workerThread 执行 stopWork()(但要注意:如果 doWork1 正在执行且没有处理事件,stopWork 也只是放入队列,不会马上执行)。因此对立即生效,直接调用 worker->stopWork() 能更快改变 isCanRun(前提是 stopWork() 不依赖于在 workerThread 中执行的上下文)。


六、doWork1 的阻塞问题与事件循环

  • doWork1 是个长时间运行的函数,它会占用 workerThread,阻塞该线程的事件循环 (因为你在槽里做了一个 while(1) 循环),这意味着:

    • doWork1 运行期间,其他投递到 workerThread 的事件/槽(例如 stopWork() 如果通过 queued 信号投递)将等待,直到 doWork1 完成或主动调用 QCoreApplication::processEvents()
  • 因此常见建议:

    • 如果你需要在线程中同时响应外部请求(比如 stop),要在循环中定期检查一个原子标志(如 isCanRun),并尽量避免长时间不检查,这样 stopWork() 执行后可以尽快退出。

    • 或者把长期任务拆成小块,使用定时器或任务队列来处理,而不是一个无限循环。


七、现代写法:函数指针(推荐)与 lambda 替换示例

你当前用的是旧式 SIGNAL()/SLOT() 宏写法,建议用函数指针 / lambda 的现代写法,优点:编译时检查、类型安全

把这些旧写法:

cpp 复制代码
connect(&workerThread, SIGNAL(finished()),
        worker, SLOT(deleteLater()));
connect(this, SIGNAL(startWork(QString)),
        worker, SLOT(doWork1(QString)));
connect(worker, SIGNAL(resultReady(QString)),
        this, SLOT(handleResults(QString)));

替换为推荐的现代写法:

cpp 复制代码
// 推荐:使用函数指针 / 成员指针语法(编译期检查)
connect(&workerThread, &QThread::finished,
        worker, &QObject::deleteLater);

connect(this, &MainWindow::startWork,
        worker, &Worker::doWork1);

connect(worker, &Worker::resultReady,
        this, &MainWindow::handleResults);

使用 lambda(有时很方便)

如果你想在发信号时直接做一点额外处理或绑定上下文:

cpp 复制代码
connect(worker, &Worker::resultReady, this, [this](const QString &s){
    qDebug() << "从worker收到:" << s;
    // 可以在这里直接更新 UI
});

关于返回值 QMetaObject::Connection

你也可以保存连接句柄以便后续断开:

cpp 复制代码
QMetaObject::Connection c = connect(worker, &Worker::resultReady,
                                    this, &MainWindow::handleResults);

// later
disconnect(c);

八、完整现代化示例(把你的代码改用函数指针写法,做一些改进)

下面给出一个更稳健的、完整片段(只示核心部分):

cpp 复制代码
// 在头文件里确保 signals / slots 名称匹配
signals:
    void startWork(const QString &);

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private:
    QThread workerThread;   // 栈对象也可以
    Worker *worker;
cpp 复制代码
// MainWindow 构造
worker = new Worker;
worker->moveToThread(&workerThread);

// 当线程结束时删除 worker(worker 是堆对象,安全)
connect(&workerThread, &QThread::finished,
        worker, &QObject::deleteLater);

// 现代写法:MainWindow -> Worker
connect(this, &MainWindow::startWork,
        worker, &Worker::doWork1);

// Worker -> MainWindow(结果回调)
connect(worker, &Worker::resultReady,
        this, &MainWindow::handleResults);

// 按钮
connect(pushButton1, &QPushButton::clicked,
        this, &MainWindow::pushButton1Clicked);
connect(pushButton2, &QPushButton::clicked,
        this, &MainWindow::pushButton2Clicked);

析构函数保持不变:

cpp 复制代码
MainWindow::~MainWindow()
{
    worker->stopWork();     // 通知 worker 停止
    workerThread.quit();    // 请求线程退出事件循环
    if (!workerThread.wait(2000)) {
        qWarning() << "workerThread did not exit in time!";
        // 视情况处理:日志/强制退出等
    }
}

如果你想把 QThread 也放到堆上(动态创建),可以这样:

cpp 复制代码
QThread *workerThread = new QThread(this); // parent 为 MainWindow,可在 MainWindow 销毁时自动删除
Worker *worker = new Worker;
worker->moveToThread(workerThread);

connect(workerThread, &QThread::finished, worker, &QObject::deleteLater);
connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater);

workerThread->start();

注:如果 workerThreadnew 出来的堆对象,那么把 workerThreadfinished 信号连到 workerThread->deleteLater()允许且合理 的(线程对象会被安全删除)。但是当 workerThread 是栈对象(QThread workerThread;)时,绝对不要 让线程自己 deleteLater() 自己------那会导致 delete 栈对象,崩溃。


九、补充 - 关于 deleteLater() 在跨线程的语义(注意点)

  • QObject::deleteLater() 将发送一个 DeferredDelete 事件到该对象所在线程的事件循环中执行删除(也就是对象会在所属线程安全地被删除)。

  • 因此常见做法是:connect(thread, &QThread::finished, worker, &QObject::deleteLater),当线程结束时会安排删除 worker(因为 worker 的所属线程刚好是该线程)。注意:如果线程已经终止且事件循环不再处理事件,那么 deleteLater 可能无法立刻执行,但在 finished 发出时通常仍在可处理状态。另一种更强的办法是:在主线程等待 thread.wait() 完成后直接 delete worker(需要确保 worker 的线程已安全终止且不再运行)。


十、一些实践建议(小结)

  • 推荐使用函数指针&Class::signal / &Class::slot)或 lambda,避免使用宏 SIGNAL()/SLOT()

  • Worker 如果在堆上 new,用 connect(thread, &QThread::finished, worker, &QObject::deleteLater) 清理它。

  • 如果 QThread 是栈对象,不要deleteLater() 连接到它自己。

  • 在析构前:先通知 worker 停止(atomic 或 mutex),再 quit()wait()

  • 对共享标志用 QMutexstd::atomic<bool>,后者在只需要布尔读写时更高效。

  • 如果槽是长耗时函数,确保它定期检查停止条件以便能迅速退出,否则 quit()/finished 可能起不到立即效果。


如果你愿意,我可以把你当前文件(.h + .cpp全部改成现代安全风格 并贴出完整可直接编译的代码(包含 Worker 构造初始化 isCanRun、使用 std::atomic<bool>、并把 connect 全部改成函数指针写法)。要我直接给完整代码吗?

相关推荐
一叶之秋14121 小时前
Qt中的信号与槽
qt
FeiHuo565151 小时前
微信个人号API二次开发:如何提高开发效率和质量
java·开发语言·python·php
q***54751 小时前
java进阶--多线程学习
java·开发语言·学习
原野-1 小时前
PHP女程序猿学习Java的Day-5
java·开发语言·学习
z***56561 小时前
GO 快速升级Go版本
开发语言·redis·golang
矜辰所致1 小时前
C 语言 —— 函数指针
c语言·开发语言·指针·typedef·函数指针
zore_c1 小时前
【C语言】struct结构体内存对齐和位段(超详解)
c语言·开发语言·经验分享·笔记
MC皮蛋侠客1 小时前
C++17多线程编程全面指南
开发语言·c++
郝学胜-神的一滴1 小时前
Linux C++系统编程:使用mmap创建匿名映射区
linux·服务器·开发语言·c++·程序人生