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 提供的线程同步类 :QMutex、QMutexLocker、QReadLocker 和 QWriteLocker,并分析它们的使用场景和区别。
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️⃣ 使用建议
-
简单互斥 → 用
QMutex或QMutexLocker -
读多写少 → 用
QReadWriteLock+QReadLocker/QWriteLocker -
RAII 风格优先 → 使用 Locker,避免忘记 unlock
-
不要在 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 对象 就像一个 "家庭成员"。
-
出生地原则(Creation Thread):
QObject在哪个线程中被创建,它就天然地"出生"并属于 那个线程。这是它的初始亲和性。
-
独居与派对(Event Loop):
-
每个线程(家)如果想要处理 信号与槽的异步连接 (
Qt::QueuedConnection)或 定时器事件 、网络事件 等,它就必须有一个 事件循环(Event Loop) ,就像家里的一个"派对/待办事项处理中心"。 -
QThread 的
run()方法 默认会启动一个事件循环。
-
-
搬家(
moveToThread()):-
QObject对象可以通过调用obj->moveToThread(targetThread)来"搬家",改变它所属于的线程。 -
限制: 如果这个"家庭成员"已经有了一个"家长"(即设置了
parent),那么它就不能再搬家了。有父对象(Parent)的 QObject 必须和它的父对象待在同一个线程中。
-
⚠️ 为什么不应该直接继承 QThread 来执行任务?
传统的(但不推荐的)Qt 线程用法是继承 QThread 并重写 run()。但是,QThread 对象本身 是线程的控制器/管理者 ,它通常属于创建它的线程 (比如主线程),而不是它所运行的新线程。
错误示范: 如果你直接在继承
QThread的子类中添加QObject成员和槽函数,你可能会在新线程 中执行任务,但这些槽函数(因为它继承自QObject)却可能在 主线程 中被调用,造成线程安全问题!
✅ 推荐做法:Worker 模式
最安全和推荐的做法是使用 Worker 对象模式:
-
创建 Worker: 创建一个继承自
QObject的类 (Worker),它包含你想要在新线程中执行的任务代码。 -
创建 Thread: 创建一个
QThread实例。 -
搬家: 调用
worker->moveToThread(thread),将Worker对象"搬"到新线程中。 -
启动: 调用
thread->start()启动线程(开启 Worker 的新家)。 -
信号/槽通信: 在主线程(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 ¶meter);
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 ¶meter)
{
// 任务执行时,检查当前线程 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 |
槽函数指针 | 槽函数 | 执行什么操作来响应信号。这是接收者对象的一个成员函数,当信号发出时它会被自动执行。 |
结构如下:
-
什么是 connect
-
四种写法(宏版、函数指针版、lambda版、自动连接)
-
参数规则
-
线程行为
-
常见错误
-
最推荐用法
① 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 ¶meter) {
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)写法的完整示例和替换方法。
一、总体运行脉络(高层流程概览)
-
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()。
-
-
点击"开启线程"按钮
-
如果
workerThread未运行:workerThread.start()启动线程(并在该线程中运行事件循环)。 -
emit startWork("正在运行"):发射信号,Qt 根据线程归属决定调用方式。因为MainWindow(sender)在主线程、worker(receiver)在workerThread,默认是Qt::AutoConnection→ 会变成Qt::QueuedConnection(即把调用封装成事件投递到workerThread的事件队列)。
-
-
Worker::doWork1在workerThread执行-
doWork1被事件队列调度到 workerThread 上执行(槽函数在该线程上下文中运行)。 -
在
doWork1中循环每 2 秒emit resultReady(...),这会把信号发回主线程(因为MainWindow::handleResults在主线程)------同样是队列调用,handleResults 在主线程运行,打印日志。
-
-
点击"打断线程"按钮
-
主线程调用
worker->stopWork()(该函数取得互斥锁,设置isCanRun=false),doWork1在下一轮循环检查锁后看到isCanRun==false就跳出循环并退出槽函数。 -
doWork1结束后,若需要退出线程的事件循环,应由主线程调用workerThread.quit()(退出事件循环)或由 worker 自己决定(通常由主线程调用quit())。然后workerThread.wait()阻塞等待线程结束。
-
-
析构(MainWindow::~MainWindow)
- 典型做法:先通知 worker 停止(
worker->stopWork()),再workerThread.quit(),再workerThread.wait(...)等待线程结束。这样可以保证线程内的槽运行结束、资源安全释放,避免在 MainWindow 或应用退出时出现悬挂/竞态/崩溃。
- 典型做法:先通知 worker 停止(
二、关键函数/动作之间的具体联系(谁调用谁、在哪个线程)
-
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;
}
}
解释:
-
worker->stopWork()- 告诉 worker 停止循环 (设置
isCanRun=false),让doWork1能够尽快退出循环并返回。这一步是优雅结束工作的重要信号;如果不做,doWork1可能一直阻塞,线程永远不会结束。
- 告诉 worker 停止循环 (设置
-
workerThread.quit()- 请求线程的事件循环退出(如果线程正在运行事件循环)。如果
doWork1正在运行且阻塞,这个quit()只会在事件循环空闲时生效;因此必须先让 worker 自行退出循环。
- 请求线程的事件循环退出(如果线程正在运行事件循环)。如果
-
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=false与doWork1里读的竞态)。 -
另外的更简洁/高效的写法是使用
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();
注:如果
workerThread是new出来的堆对象,那么把workerThread的finished信号连到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()。 -
对共享标志用
QMutex或std::atomic<bool>,后者在只需要布尔读写时更高效。 -
如果槽是长耗时函数,确保它定期检查停止条件以便能迅速退出,否则
quit()/finished可能起不到立即效果。
如果你愿意,我可以把你当前文件(.h + .cpp)全部改成现代安全风格 并贴出完整可直接编译的代码(包含 Worker 构造初始化 isCanRun、使用 std::atomic<bool>、并把 connect 全部改成函数指针写法)。要我直接给完整代码吗?