当前位置: 首页 > ops >正文

Qt5多线程编程详细讲解

个人博客:blogs.wurp.top

一、核心概念与基础类

在 Qt 中,多线程编程的核心是 QThread 类。一个 QThread 对象就代表了一个程序中的线程。

  • 主线程 (GUI线程): 程序启动时自动创建的线程。所有窗口组件 (QWidget, QMainWindow 等) 都必须创建和运行在这个线程中。在这里执行耗时操作会阻塞界面,导致程序“卡死”。
  • 工作线程 (子线程): 由主线程创建的线程。用于执行后台的、耗时的任务,如文件读写、网络通信、复杂计算等,从而保证主线程的流畅。

重要原则: 永远不要在非主线程中操作或创建任何 GUI 组件。


二、实现多线程的两种主要方式

Qt5 推荐的方式与 Qt4 早期的方式有所不同。我们会详细介绍这两种,并重点讲解现代推荐的方式。

方式一:继承 QThread 并重写 run() 函数 (传统方式,Qt4 风格)

这是最直观的方式,但在 Qt5 中已不被推荐为首选。它源于 Java 的 Thread 类设计。

工作原理: 你创建一个 QThread 的子类,并重写其 run() 方法。run() 函数内的代码将在新线程中执行。调用 start() 方法后,线程开始执行,run() 函数被调用,run() 函数返回后,线程结束。

示例代码:

// myworkerthread.h
#include <QThread>class MyWorkerThread : public QThread
{Q_OBJECT // 必须包含,因为要用到信号槽protected:void run() override; // 核心,重写 run 函数signals:void resultReady(const QString &result); // 用于向主线程发送结果的信号private:// 可以在这里定义线程需要的数据
};// myworkerthread.cpp
#include "myworkerthread.h"
#include <QDebug>
#include <QElapsedTimer> // 用于模拟耗时操作void MyWorkerThread::run()
{qDebug() << "Worker thread started. Thread ID:" << QThread::currentThreadId();// 1. 模拟一个耗时操作QElapsedTimer timer;timer.start();while(timer.elapsed() < 5000) { // 模拟 5 秒工作// 做一些事情...msleep(500); // 睡眠 500 毫秒,模拟工作片段// 注意:可以在这里检查 isInterruptionRequested() 来安全退出}// 2. 工作完成,发出信号QString result = “Task completed in 5 seconds”;emit resultReady(result);qDebug() << “Worker thread finished.;
}

在主线程中使用:

// mainwindow.cpp
#include “mainwindow.h”
#include “myworkerthread.h”
#include <QDebug>MainWindow::MainWindow(QWidget *parent): QMainWindow(parent)
{// 创建线程对象m_thread = new MyWorkerThread(this);// 连接线程完成后的信号connect(m_thread, &MyWorkerThread::resultReady, this, &MainWindow::handleResults);connect(m_thread, &MyWorkerThread::finished, m_thread, &QObject::deleteLater); // 线程结束后自动删除对象// 按钮点击开始线程QPushButton *button = new QPushButton(“Start Task”, this);connect(button, &QPushButton::clicked, [this]() {qDebug() << “Main thread ID:<< QThread::currentThreadId();if(!m_thread->isRunning()) {m_thread->start(); // 启动线程,调用 start(),而不是直接调用 run()}});
}MainWindow::~MainWindow()
{// 优雅地退出线程m_thread->quit(); // 请求线程退出事件循环 (如果用了事件循环)// m_thread->terminate(); // 强制终止,非常危险,不推荐!m_thread->wait(); // 等待线程真正结束
}void MainWindow::handleResults(const QString &result)
{// 这个槽函数在主线程中执行qDebug() << “Result received:<< result;// 更新 UI...
}

缺点

  1. run() 函数是受保护的,你很难在其外部与正在运行的线程对象进行交互(例如,调用其自定义方法)。
  2. 所有成员变量和函数都在新线程的上下文中,容易引发线程安全问题。
  3. 不符合 Qt 的“事件驱动”设计哲学。

方式二:使用 Worker Object + moveToThread (推荐方式,Qt5 风格)

这是 Qt5 官方推荐的方式。它的核心思想是:将要在子线程中执行的逻辑封装在一个 QObject 派生类(Worker Object)中,然后利用 QObject::moveToThread() 方法将这个对象移动到另一个线程里。

工作原理

  1. 主线程创建 QThread 对象和工作者对象 (Worker)。
  2. 主线程调用 worker->moveToThread(thread),这将使得 worker 的所有槽函数在 thread 线程中被调用。
  3. 主线程通过信号槽机制向 worker 对象发送信号,触发其在子线程中执行任务。
  4. worker 对象完成任务后,通过信号将结果发送回主线程(通常是窗口对象)。

示例代码:

// worker.h
#include <QObject>class Worker : public QObject
{Q_OBJECTpublic slots:void doWork(const QString ¶meter); // 执行耗时任务的槽函数signals:void workStarted();void resultReady(const QString &result);void workFinished();
};// worker.cpp
#include “worker.h”
#include <QDebug>
#include <QThread>
#include <QElapsedTimer>void Worker::doWork(const QString ¶meter)
{qDebug() << “Worker::doWork running in thread:<< QThread::currentThreadId();emit workStarted();// 模拟耗时任务QElapsedTimer timer;timer.start();QString result;while(timer.elapsed() < 5000) {int progress = (timer.elapsed() * 100) / 5000;// 可以发射进度信号...// emit progressUpdated(progress);QThread::msleep(500);result = QString(“Processed ‘%1for 5 seconds”).arg(parameter);}emit resultReady(result);emit workFinished(); // 任务完成信号
}

在主线程中使用:

// mainwindow.h
#include <QMainWindow>
#include <QThread>
#include “worker.h”class MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private slots:void startTask();void handleResults(const QString &result);void onWorkerFinished();private:Worker *m_worker;QThread *m_workerThread;
};// mainwindow.cpp
#include “mainwindow.h”
#include <QPushButton>
#include <QDebug>MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), m_worker(nullptr), m_workerThread(nullptr)
{// 1. 创建工作者对象和线程对象// **注意:Worker 对象不能指定 parent,因为它的生命周期将由新线程管理**m_worker = new Worker;m_workerThread = new QThread;// 2. 将工作者对象移动到新线程m_worker->moveToThread(m_workerThread);// 3. 连接信号槽// 连接线程开始的信号,来触发工作者的槽函数connect(m_workerThread, &QThread::started, m_worker, [this]() { m_worker->doWork(“Hello Thread”); });// 连接工作者的结果信号到主窗口的槽connect(m_worker, &Worker::resultReady, this, &MainWindow::handleResults);// 连接工作者的结束信号,用来退出线程的事件循环connect(m_worker, &Worker::workFinished, m_workerThread, &QThread::quit);// 连接线程结束的信号,让工作者对象自动删除connect(m_workerThread, &QThread::finished, m_worker, &QObject::deleteLater);// 连接线程结束的信号,让线程对象自身自动删除connect(m_workerThread, &QThread::finished, m_workerThread, &QObject::deleteLater);QPushButton *button = new QPushButton(“Start Task”, this);connect(button, &QPushButton::clicked, this, &MainWindow::startTask);
}MainWindow::~MainWindow()
{// 如果线程还在运行,请求退出if (m_workerThread && m_workerThread->isRunning()) {m_workerThread->quit();m_workerThread->wait();}// 注意:m_worker 和 m_workerThread 会被上面的 deleteLater 自动删除,所以这里不需要手动 delete。
}void MainWindow::startTask()
{qDebug() << “Main thread ID:<< QThread::currentThreadId();if (m_workerThread && !m_workerThread->isRunning()) {m_workerThread->start(); // 启动线程的事件循环}
}void MainWindow::handleResults(const QString &result)
{// 这个槽在主线程执行,可以安全操作 UIqDebug() << “Result:<< result;
}void MainWindow::onWorkerFinished()
{qDebug() << “Worker has finished.;
}

优点

  1. 更符合 Qt 事件驱动模型:通过信号槽驱动,代码更清晰。
  2. 更好的控制力:你可以随时从主线程向工作对象发送信号,控制其行为(如开始、暂停、停止)。
  3. 资源管理更清晰:工作对象和线程对象的生命周期可以通过 deleteLater 安全地管理。
  4. 一个线程可以服务多个工作对象,更加灵活。

三、线程同步与通信

多个线程访问共享资源时,必须进行同步,否则会导致数据竞争和不一致。

  1. 信号槽 (Signals & Slots)这是 Qt 中线程间通信的首选和最主要机制。它是线程安全的。当一个信号连接到一个槽,且连接类型为 Qt::AutoConnection(默认)时,Qt 会自动判断信号发射者和接收者是否在同一线程。如果在不同线程,信号会被转换为一个事件(Event),放入接收者线程的事件循环(Event Loop)中等待处理,从而实现线程安全的跨线程调用。

  2. 互斥锁 (QMutex): 保护一段代码(临界区),一次只允许一个线程访问。

    QMutex mutex;
    int number;void threadSafeFunction() {mutex.lock();number++; // 临界区操作mutex.unlock();
    }
    

    推荐使用 QMutexLocker 进行自动加锁解锁,避免忘记解锁。

    void threadSafeFunction() {QMutexLocker locker(&mutex);number++; // locker 析构时会自动解锁 mutex
    }
    
  3. 读写锁 (QReadWriteLock): 允许多个线程同时读,但写操作是独占的。适用于“读多写少”的场景。

    QReadWriteLock lock;
    QString data;QString readData() {QReadLocker reader(&lock);return data;
    }void writeData(const QString &newData) {QWriteLocker writer(&lock);data = newData;
    }
    
  4. 信号量 (QSemaphore): 用于保护一定数量的相同资源。

    QSemaphore semaphore(5); // 保护 5 个资源void useResource() {semaphore.acquire(); // 获取一个资源,如果没有则阻塞// ... 使用资源 ...semaphore.release(); // 释放一个资源
    }
    
  5. 等待条件 (QWaitCondition): 允许线程在某些条件满足时才继续执行,常用于生产者-消费者模型。

    QWaitCondition condition;
    QMutex mutex;
    bool dataReady = false;// 生产者线程
    void producer() {mutex.lock();// ... 生产数据 ...dataReady = true;condition.wakeAll(); // 或 wakeOne()mutex.unlock();
    }// 消费者线程
    void consumer() {mutex.lock();while (!dataReady) {condition.wait(&mutex); // 解锁 mutex 并等待,被唤醒后重新锁定 mutex}// ... 消费数据 ...dataReady = false;mutex.unlock();
    }
    

四、其他高级用法

  1. 线程池 (QThreadPool) 和 QRunnable
    对于大量、短生命周期的并行任务,频繁创建销毁线程开销很大。QThreadPool 管理着一组可重用的线程。你只需要继承 QRunnable 并实现 run() 方法,然后交给 QThreadPool::start() 即可。

    class MyTask : public QRunnable {void run() override {// 任务代码}
    };
    MyTask *task = new MyTask;
    QThreadPool::globalInstance()->start(task);
    
  2. QtConcurrent 框架
    这是一个更高级的 API,用于编写多线程程序,无需直接使用低级线程原语。它特别适合对容器中的元素进行并行处理(如 map, filter, reduce)。

    #include <QtConcurrent/QtConcurrentMap>QString processItem(const QString &item) {return item.toUpper();
    }// 在主线程中
    QStringList list = {“a”, “b”, “c”};
    QFuture<QString> future = QtConcurrent::map(list, processItem); // 异步执行
    future.waitForFinished(); // 等待完成
    QStringList results = future.results();
    

五、总结与注意事项

特性/方式继承 QThreadWorker Object + moveToThread
设计理念基于继承基于组合、事件驱动
灵活性较低高,易于控制和管理
推荐度不推荐为首选Qt5 官方推荐
适用场景非常简单的后台任务复杂的、需要交互的后台任务

重要注意事项

  • 不要跨线程操作 GUI:这是铁律。
  • 线程安全:默认情况下,QObject 是线程不安全的。确保对共享数据的访问使用同步原语(如 QMutex)。
  • 事件循环QThreadrun() 默认调用 exec() 来启动一个事件循环。moveToThread 方式必须要有事件循环才能工作,因为信号槽通信依赖于它。
  • 优雅退出:使用 quit()wait() 来请求线程退出并等待,而不是强制 terminate()
  • 对象树:小心对象的父子关系。移动到新线程的对象最好不要有父对象,其删除应使用 deleteLater(),这会在该对象所在线程的事件循环中安全地删除它。

希望这份详细的讲解能帮助你全面掌握 Qt5 的多线程编程!

http://www.xdnf.cn/news/18224.html

相关文章:

  • [递归回溯]679. 24 点游戏
  • 基于RK3568/J6412的EMU多网口控制主机,助力储能工业互联管理和运维
  • PyTorch 社区贡献 和 设计原则
  • 第5课_Rust生命周期和泛型
  • Android MVVM(Model-View-ViewModel)架构
  • 从零开始的云计算生活——第四十七天,细水长流,kubernetes模块之ingress资源对象
  • 23TaskExecutor初始化
  • 【ansible】4.实施任务控制
  • AI 伦理的 “灰色地带”:当算法拥有决策权,公平与隐私该如何平衡?
  • 工地智能安全带让高空作业更安全
  • Kafka如何保证消费确认与顺序消费?
  • gcc 与 g++ 的区别:本身不是编译器而是编译器驱动
  • 数据库优化提速(一)之进销存库存管理—仙盟创梦IDE
  • 【Tech Arch】Apache Pig大数据处理的高效利器
  • 【JavaEE】多线程 -- 线程池
  • 基于单片机太阳能充电器/太阳能转换电能
  • 30. 技术专题-锁
  • HTTP的协议
  • .gitignore 文件 记录
  • Linux服务器性能优化总结
  • 【Tech Arch】Apache HBase分布式 NoSQL 数据库
  • redis---常用数据类型及内部编码
  • 如何低比特量化算法的工程实战与落地优化
  • 【考研408数据结构-08】 图论基础:存储结构与遍历算法
  • 让Chrome信任自签名证书
  • AI时代下阿里云基础设施的稳定性架构揭秘
  • C#海康SDK—热成像测温篇
  • gitlab、jenkins等应用集成ldap
  • package.json详细字段解释
  • 大数据技术栈 —— Redis与Kafka