第六章 QT基础:7、Qt中多线程的使用
在进行桌面应用程序开发时,假设应用程序需要处理比较复杂的逻辑,如果只有一个线程去处理,就会导致窗口卡顿,无法处理用户的相关操作。
这种情况下,需要使用多线程:
- 主线程处理窗口事件和控件更新
- 子线程进行后台逻辑处理
- 多线程协作提升用户体验和执行效率
多线程开发注意事项
- Qt默认的线程叫做窗口线程(主线程),负责处理窗口控件和界面更新。
- 子线程负责后台的业务逻辑,子线程中不能直接操作窗口控件。
- 主线程和子线程之间的数据通信,使用信号与槽机制完成。
1. 线程类 QThread
Qt 提供了 QThread
类来创建子线程,有两种常用的子线程使用方式。
1.1 常用成员函数
// 构造函数,创建一个线程对象
QThread::QThread(QObject *parent = Q_NULLPTR);// 查询线程是否结束(任务执行完毕)
bool QThread::isFinished() const;// 查询线程是否正在运行
bool QThread::isRunning() const;// 设置和获取线程优先级
Priority QThread::priority() const;
void QThread::setPriority(Priority priority);// 退出线程(退出事件循环)
void QThread::exit(int returnCode = 0);// 等待线程结束(通常配合exit()使用)
bool QThread::wait(unsigned long time = ULONG_MAX);
1.2 信号与槽
// 请求线程正常退出(发送退出指令)
[slot] void QThread::quit();// 启动线程,内部自动调用run()
[slot] void QThread::start(Priority priority = InheritPriority);// 强制终止线程(不推荐,可能导致资源泄漏)
[slot] void QThread::terminate();// 线程执行完成时发出的信号
[signal] void QThread::finished();// 线程开始运行时发出的信号
[signal] void QThread::started();
1.3 静态函数
// 获取当前执行的线程指针
[static] QThread* QThread::currentThread();// 获取当前系统的理想线程数(通常等于CPU核心数)
[static] int QThread::idealThreadCount();// 线程休眠函数
[static] void QThread::msleep(unsigned long msecs); // 毫秒级休眠
[static] void QThread::sleep(unsigned long secs); // 秒级休眠
[static] void QThread::usleep(unsigned long usecs); // 微秒级休眠
1.4 任务处理函数
// 子线程逻辑的入口函数,需要重写
[virtual protected] void QThread::run();
- 注意
run()
是 虚函数,只能通过子类重写。 - 不要手动调用
run()
,应通过start()
启动线程,让Qt自动调用run()
。
2. 使用方式一:继承QThread重写run()
2.1 操作步骤
[!important]
- 创建一个子类继承自
QThread
- 重写
run()
函数,写入业务逻辑- 在主线程中 new 子线程对象
- 调用
start()
启动子线程
2.2 示例代码(含详细注释)
mythread.h
#ifndef MYTHREAD_H
#define MYTHREAD_H#include <QThread>// 自定义线程类,继承自QThread
class MyThread : public QThread
{Q_OBJECT
public:explicit MyThread(QObject *parent = nullptr); // 构造函数protected:void run() override; // 重写run(),处理子线程任务//子线程的`run()`是由系统自动调用的,不应该让别人直接调用,否则容易破坏线程机制signals:void curNumber(int num); // 自定义信号,用于传递当前数字
};#endif // MYTHREAD_H
mythread.cpp
#include "mythread.h"
#include <QDebug>MyThread::MyThread(QObject *parent) : QThread(parent) {}
//第一个MyThread::是定义的是属于 `MyThread` 这个类的东西
//第二个MyThread构造函数的名字,在C++里构造函数名字必须和类名一模一样void MyThread::run()
{qDebug() << "当前线程对象地址:" << QThread::currentThread();int num = 0;while (1){emit curNumber(num++); // 每次加一并发出信号if (num == 10000000) // 数到一定数量退出break;QThread::usleep(1); // 每次循环稍作休眠,减少CPU占用}qDebug() << "run()执行完毕,子线程退出...";
}
mainwindow.cpp
#include "mainwindow.h" // 引入主窗口头文件
#include "ui_mainwindow.h" // 引入主窗口UI界面头文件
#include "mythread.h" // 引入自定义线程类头文件
#include <QDebug> // 引入Qt调试输出模块// 主窗口的构造函数
// 使用初始化列表,调用父类QMainWindow的构造函数MainWindow,并传入parent对象指针
//parent的主要作用是建立父子对象关系,方便自动内存管理!
MainWindow::MainWindow(QWidget *parent)//子类名::构造函数名(参数): QMainWindow(parent), // 1. 调用父类QMainWindow的构造函数,传入parentui(new Ui::MainWindow) // 2. 初始化成员变量ui,new出一个新的Ui::MainWindow对象
{ui->setupUi(this); // 3. 在构造体内部,初始化界面// 输出当前线程对象的地址// QThread::currentThread() 返回当前正在运行的线程指针qDebug() << "主线程对象地址: " << QThread::currentThread();// 创建自定义子线程对象// 注意:这里没有设置父对象,生命周期需要自己管理,或合理搭配QObject父子机制MyThread* subThread = new MyThread;// 连接子线程发出的curNumber信号到主线程的槽函数(用lambda表达式)// 作用:收到子线程发的数字信号后,更新UI界面label上的数值connect(subThread, &MyThread::curNumber, this, [=](int num){ui->label->setNum(num); // 把收到的数字设置到界面上的label控件});// 连接“开始”按钮的点击信号到槽函数(用lambda表达式)// 作用:点击按钮时启动子线程connect(ui->startBtn, &QPushButton::clicked, this, [=](){subThread->start(); // 启动子线程,内部自动调用子线程的run()方法});
}// 主窗口析构函数
// 作用:释放UI资源
MainWindow::~MainWindow()
{delete ui;
}
3. 使用方式二:QObject+moveToThread()
3.1 操作步骤
[!important]
- 创建一个普通类继承自
QObject
- 定义公共函数作为业务处理函数(如 working)
- 创建
QThread
对象- 创建工作对象(不要设置父对象)
- 使用
moveToThread()
把工作对象移到子线程- 启动子线程,绑定信号槽调用工作函数
3.2 示例代码(含详细注释)
mywork.h
#ifndef MYWORK_H
#define MYWORK_H#include <QObject>// 自定义工作对象
class MyWork : public QObject
{Q_OBJECT
public:explicit MyWork(QObject *parent = nullptr);void working(); // 工作逻辑函数signals:void curNumber(int num); // 自定义信号传递当前数字
};#endif // MYWORK_H
mywork.cpp
#include "mywork.h" // 引入自定义工作类头文件
#include <QDebug> // 引入Qt调试打印模块
#include <QThread> // 引入Qt线程模块,使用currentThread()和usleep()// 构造函数实现
// 使用初始化列表方式,调用父类QObject的构造函数,传入parent指针
MyWork::MyWork(QObject *parent) : QObject(parent) {}// 自定义的工作函数,具体执行子线程任务
void MyWork::working()
{// 打印当前执行working()的线程对象指针// QThread::currentThread()是静态函数,返回当前运行线程的指针qDebug() << "当前线程对象地址:" << QThread::currentThread();int num = 0; // 定义计数变量,从0开始// 无限循环while (1){//在子线程里处理数据后,及时把处理结果发送(通知)给主线程。emit curNumber(num++); // 每次计数后,发射curNumber信号,把当前num值发出去// 如果计数到10000000,跳出循环,结束工作if (num == 10000000)break;QThread::usleep(1); // 休眠1微秒,避免CPU占用过高(防止死循环卡死)}// 循环结束,任务完成,打印提示qDebug() << "working()执行完毕,子线程退出...";
}
mainwindow.cpp
#include "mainwindow.h" // 引入主窗口头文件
#include "ui_mainwindow.h" // 引入自动生成的UI界面头文件
#include <QThread> // 引入Qt线程模块
#include "mywork.h" // 引入自定义工作类头文件
#include <QDebug> // 引入Qt调试打印模块// 主窗口构造函数
// 使用初始化列表调用父类QMainWindow构造函数,并创建UI对象
MainWindow::MainWindow(QWidget *parent) :QMainWindow(parent),ui(new Ui::MainWindow)
{ui->setupUi(this); // 初始化界面控件// 打印当前线程对象地址(主线程)qDebug() << "主线程对象地址:" << QThread::currentThread();// 创建子线程对象(QThread对象)QThread* sub = new QThread;// 创建工作对象(自定义的MyWork类实例)// 注意:不能设置parent,否则moveToThread会失败!MyWork* work = new MyWork;// 将工作对象移动到子线程中执行// 注意:只能移动继承自QObject的对象work->moveToThread(sub);// 启动子线程// 此时QThread内部开启一个空事件循环(event loop),为工作对象提供子线程环境sub->start();// 连接按钮点击信号到工作对象的working槽函数// 作用:点击按钮后,让work开始执行working()方法connect(ui->startBtn, &QPushButton::clicked, work, &MyWork::working);//connect(发送者, 发送者的信号, 接收者, 接收者的槽函数);//&MyWork::working指向 `MyWork` 类的成员函数 `working()`//- Qt在不同线程之间发信号自动切换线程(通过队列连接QueueConnection)。 //- 子线程不能直接操作界面,但发信号给主线程处理是安全的!// 连接工作对象发出的curNumber信号到主线程的槽// 作用:收到子线程发回的数字后,在界面label上显示connect(work, &MyWork::curNumber, this, [=](int num){ui->label->setNum(num);});
}// 主窗口析构函数
// 释放UI资源
MainWindow::~MainWindow()
{delete ui;
}
connect(ui->startBtn, &QPushButton::clicked, work, &MyWork::working);
![[Pasted image 20250428134905.png]]
connect(work, &MyWork::curNumber, this, [=](int num){ui->label->setNum(num);
});
![[Pasted image 20250428134855.png]]
4. 小结
使用方式 | 优点 | 缺点 |
---|---|---|
继承QThread重写run | 简单直观,适合小任务 | run()过于臃肿时维护困难 |
QObject+moveToThread | 灵活,适合多个任务类 | 写法复杂,要求掌握对象迁移 |
5. 版权信息
- 作者: 苏丙榅
- 链接: https://subingwen.cn/qt/thread/#3-2-示例代码
- 来源: 爱编程的大丙
- 版权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。
✅ 示例场景:多线程文件拷贝(防止界面卡顿)
copyworker.h
#ifndef COPYWORKER_H
#define COPYWORKER_H#include <QObject>class CopyWorker : public QObject
{Q_OBJECT
public:explicit CopyWorker(QObject *parent = nullptr);void setFilePaths(const QString &src, const QString &dest);public slots:void startCopy(); // 复制操作入口(槽函数)signals:void progress(int percent); // 进度更新信号void finished(); // 完成信号void error(QString msg); // 错误信息
private:QString m_srcFilePath;QString m_destFilePath;
};#endif // COPYWORKER_H
copyworker.cpp
#include "copyworker.h"
#include <QFile>
#include <QFileInfo>
#include <QThread>CopyWorker::CopyWorker(QObject *parent): QObject(parent)
{}void CopyWorker::setFilePaths(const QString &src, const QString &dest)
{m_srcFilePath = src;m_destFilePath = dest;
}void CopyWorker::startCopy()
{QFile srcFile(m_srcFilePath);QFile destFile(m_destFilePath);if (!srcFile.open(QIODevice::ReadOnly)) {emit error("源文件无法打开!");return;}if (!destFile.open(QIODevice::WriteOnly)) {emit error("目标文件无法创建!");return;}QFileInfo info(srcFile);qint64 totalSize = info.size();qint64 copied = 0;const qint64 bufferSize = 1024 * 1024; // 1MB缓冲while (!srcFile.atEnd()) {QByteArray data = srcFile.read(bufferSize);destFile.write(data);copied += data.size();int percent = static_cast<int>((copied * 100.0) / totalSize);emit progress(percent); // 发射当前进度QThread::msleep(50); // 模拟耗时}srcFile.close();destFile.close();emit finished(); // 复制完成信号
}
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "copyworker.h"#include <QThread>
#include <QFileDialog>
#include <QMessageBox>MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);// 创建线程和工作对象QThread* thread = new QThread(this);CopyWorker* worker = new CopyWorker;worker->moveToThread(thread);connect(ui->btnSelectFile, &QPushButton::clicked, this, [=] {QString file = QFileDialog::getOpenFileName(this, "选择源文件");ui->lineEditSource->setText(file);});connect(ui->btnSelectTarget, &QPushButton::clicked, this, [=] {QString file = QFileDialog::getSaveFileName(this, "选择目标路径");ui->lineEditDest->setText(file);});connect(ui->btnStartCopy, &QPushButton::clicked, this, [=] {QString src = ui->lineEditSource->text();QString dest = ui->lineEditDest->text();if (src.isEmpty() || dest.isEmpty()) {QMessageBox::warning(this, "提示", "请填写完整路径");return;}worker->setFilePaths(src, dest);emit startWork(); // 发出信号让子线程开始执行});// 绑定信号到槽函数(使用队列连接,线程安全)connect(this, &MainWindow::startWork, worker, &CopyWorker::startCopy);connect(worker, &CopyWorker::progress, this, [=](int p){ui->progressBar->setValue(p);});connect(worker, &CopyWorker::finished, this, [=]{QMessageBox::information(this, "完成", "文件复制完成!");});connect(worker, &CopyWorker::error, this, [=](const QString &msg){QMessageBox::critical(this, "错误", msg);});thread->start(); // 启动线程
}MainWindow::~MainWindow()
{delete ui;
}
mainwindow.h
信号声明
signals:void startWork(); // 让子线程开始执行的信号
UI界面组件要求
lineEditSource
:显示源路径lineEditDest
:显示目标路径btnSelectFile
:选择源文件按钮btnSelectTarget
:选择目标按钮btnStartCopy
:启动复制按钮progressBar
:进度条(范围0~100)
✅ 技术要点总结
技术点 | 说明 |
---|---|
QThread + QObject | 实现真正的工作线程架构 |
moveToThread() | 将工作对象迁移到线程中 |
信号槽机制 | 主线程与子线程通信(线程安全) |
UI线程与逻辑分离 | 不阻塞界面,提升用户体验 |
线程安全UI更新 | 使用信号更新界面,避免子线程直接操作控件 |