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...
}
缺点:
run()
函数是受保护的,你很难在其外部与正在运行的线程对象进行交互(例如,调用其自定义方法)。- 所有成员变量和函数都在新线程的上下文中,容易引发线程安全问题。
- 不符合 Qt 的“事件驱动”设计哲学。
方式二:使用 Worker Object + moveToThread (推荐方式,Qt5 风格)
这是 Qt5 官方推荐的方式。它的核心思想是:将要在子线程中执行的逻辑封装在一个 QObject
派生类(Worker Object)中,然后利用 QObject::moveToThread()
方法将这个对象移动到另一个线程里。
工作原理:
- 主线程创建
QThread
对象和工作者对象 (Worker
)。 - 主线程调用
worker->moveToThread(thread)
,这将使得worker
的所有槽函数在thread
线程中被调用。 - 主线程通过信号槽机制向
worker
对象发送信号,触发其在子线程中执行任务。 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 ‘%1’ for 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.”;
}
优点:
- 更符合 Qt 事件驱动模型:通过信号槽驱动,代码更清晰。
- 更好的控制力:你可以随时从主线程向工作对象发送信号,控制其行为(如开始、暂停、停止)。
- 资源管理更清晰:工作对象和线程对象的生命周期可以通过
deleteLater
安全地管理。 - 一个线程可以服务多个工作对象,更加灵活。
三、线程同步与通信
多个线程访问共享资源时,必须进行同步,否则会导致数据竞争和不一致。
-
信号槽 (Signals & Slots): 这是 Qt 中线程间通信的首选和最主要机制。它是线程安全的。当一个信号连接到一个槽,且连接类型为
Qt::AutoConnection
(默认)时,Qt 会自动判断信号发射者和接收者是否在同一线程。如果在不同线程,信号会被转换为一个事件(Event),放入接收者线程的事件循环(Event Loop)中等待处理,从而实现线程安全的跨线程调用。 -
互斥锁 (QMutex): 保护一段代码(临界区),一次只允许一个线程访问。
QMutex mutex; int number;void threadSafeFunction() {mutex.lock();number++; // 临界区操作mutex.unlock(); }
推荐使用
QMutexLocker
进行自动加锁解锁,避免忘记解锁。void threadSafeFunction() {QMutexLocker locker(&mutex);number++; // locker 析构时会自动解锁 mutex }
-
读写锁 (QReadWriteLock): 允许多个线程同时读,但写操作是独占的。适用于“读多写少”的场景。
QReadWriteLock lock; QString data;QString readData() {QReadLocker reader(&lock);return data; }void writeData(const QString &newData) {QWriteLocker writer(&lock);data = newData; }
-
信号量 (QSemaphore): 用于保护一定数量的相同资源。
QSemaphore semaphore(5); // 保护 5 个资源void useResource() {semaphore.acquire(); // 获取一个资源,如果没有则阻塞// ... 使用资源 ...semaphore.release(); // 释放一个资源 }
-
等待条件 (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(); }
四、其他高级用法
-
线程池 (QThreadPool) 和 QRunnable:
对于大量、短生命周期的并行任务,频繁创建销毁线程开销很大。QThreadPool
管理着一组可重用的线程。你只需要继承QRunnable
并实现run()
方法,然后交给QThreadPool::start()
即可。class MyTask : public QRunnable {void run() override {// 任务代码} }; MyTask *task = new MyTask; QThreadPool::globalInstance()->start(task);
-
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();
五、总结与注意事项
特性/方式 | 继承 QThread | Worker Object + moveToThread |
---|---|---|
设计理念 | 基于继承 | 基于组合、事件驱动 |
灵活性 | 较低 | 高,易于控制和管理 |
推荐度 | 不推荐为首选 | Qt5 官方推荐 |
适用场景 | 非常简单的后台任务 | 复杂的、需要交互的后台任务 |
重要注意事项:
- 不要跨线程操作 GUI:这是铁律。
- 线程安全:默认情况下,
QObject
是线程不安全的。确保对共享数据的访问使用同步原语(如QMutex
)。 - 事件循环:
QThread
的run()
默认调用exec()
来启动一个事件循环。moveToThread
方式必须要有事件循环才能工作,因为信号槽通信依赖于它。 - 优雅退出:使用
quit()
和wait()
来请求线程退出并等待,而不是强制terminate()
。 - 对象树:小心对象的父子关系。移动到新线程的对象最好不要有父对象,其删除应使用
deleteLater()
,这会在该对象所在线程的事件循环中安全地删除它。
希望这份详细的讲解能帮助你全面掌握 Qt5 的多线程编程!