第六章 QT基础:7、Qt中多线程的使用

发布于:2025-05-02 ⋅ 阅读:(45) ⋅ 点赞:(0)

在进行桌面应用程序开发时,假设应用程序需要处理比较复杂的逻辑,如果只有一个线程去处理,就会导致窗口卡顿,无法处理用户的相关操作。

这种情况下,需要使用多线程:

  • 主线程处理窗口事件和控件更新
  • 子线程进行后台逻辑处理
  • 多线程协作提升用户体验和执行效率

多线程开发注意事项

  • 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]

  1. 创建一个子类继承自 QThread
  2. 重写 run() 函数,写入业务逻辑
  3. 在主线程中 new 子线程对象
  4. 调用 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的构造函数,传入parent
      ui(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]

  1. 创建一个普通类继承自 QObject
  2. 定义公共函数作为业务处理函数(如 working)
  3. 创建 QThread 对象
  4. 创建工作对象(不要设置父对象
  5. 使用 moveToThread() 把工作对象移到子线程
  6. 启动子线程,绑定信号槽调用工作函数

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. 版权信息

✅ 示例场景:多线程文件拷贝(防止界面卡顿)


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更新 使用信号更新界面,避免子线程直接操作控件

网站公告

今日签到

点亮在社区的每一天
去签到