多线程编程
在 GUI 应用程序中,耗时的操作如果放在主线程执行,会导致界面卡死无响应。Qt 提供了多种多线程解决方案,本章将详细介绍如何正确使用多线程来提升应用程序的响应性和性能。
为什么需要多线程
GUI 应用程序运行在主线程(UI 线程)中,所有界面更新、事件处理都在这里执行。如果在主线程执行耗时操作:
- 界面会卡住,无法响应用户操作
- 窗口无法重绘,可能出现白屏
- 用户可能认为程序崩溃
多线程可以将耗时任务放到后台执行,保持界面流畅响应。
QThread 基础
QThread 是 Qt 中管理线程的核心类。
方式一:继承 QThread
这是最传统的方式,通过继承 QThread 并重写 run() 方法:
#include <QThread>
#include <QDebug>
// 定义工作线程类
class WorkerThread : public QThread
{
Q_OBJECT
public:
explicit WorkerThread(QObject *parent = nullptr)
: QThread(parent), m_running(true)
{
}
// 请求停止线程
void stop()
{
m_running = false;
}
protected:
void run() override
{
// run() 方法在新线程中执行
qDebug() << "工作线程开始,线程ID:" << QThread::currentThreadId();
int progress = 0;
while (m_running && progress < 100) {
// 执行耗时任务
QThread::msleep(100); // 模拟工作
progress++;
// 发射信号通知主线程
emit progressChanged(progress);
}
qDebug() << "工作线程结束";
emit finished();
}
signals:
void progressChanged(int percent);
void finished();
private:
std::atomic<bool> m_running;
};
// 使用示例
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr) : QMainWindow(parent)
{
// 创建进度条
progressBar = new QProgressBar(this);
progressBar->setRange(0, 100);
setCentralWidget(progressBar);
// 创建工作线程
worker = new WorkerThread(this);
// 连接信号(跨线程自动使用队列连接)
connect(worker, &WorkerThread::progressChanged,
this, &MainWindow::onProgressChanged);
connect(worker, &WorkerThread::finished,
this, &MainWindow::onWorkFinished);
// 启动线程
worker->start();
}
~MainWindow()
{
if (worker->isRunning()) {
worker->stop();
worker->wait(); // 等待线程结束
}
}
private slots:
void onProgressChanged(int percent)
{
progressBar->setValue(percent);
}
void onWorkFinished()
{
statusBar()->showMessage("任务完成");
}
private:
QProgressBar *progressBar;
WorkerThread *worker;
};
注意事项:
run()方法中的代码在新线程执行- 其他方法(如
stop())在调用线程执行 - 使用
std::atomic或互斥锁保护共享数据 - 跨线程的信号槽连接会自动使用
Qt::QueuedConnection
方式二:Worker-Object 模式(推荐)
这种方式更灵活,将工作逻辑与线程管理分离:
#include <QThread>
#include <QTimer>
// 工作对象类
class Worker : public QObject
{
Q_OBJECT
public:
explicit Worker(QObject *parent = nullptr) : QObject(parent)
{
}
public slots:
void doWork(const QString ¶meter)
{
// 这个方法会在工作线程中执行
qDebug() << "工作线程ID:" << QThread::currentThreadId();
qDebug() << "参数:" << parameter;
for (int i = 0; i <= 100; i++) {
QThread::msleep(50);
emit progressChanged(i);
}
emit workFinished("任务完成");
}
signals:
void progressChanged(int percent);
void workFinished(const QString &result);
};
// 控制器类
class Controller : public QObject
{
Q_OBJECT
public:
explicit Controller(QObject *parent = nullptr) : QObject(parent)
{
worker = new Worker;
workerThread = new QThread(this);
// 将 worker 移动到工作线程
worker->moveToThread(workerThread);
// 连接信号
connect(workerThread, &QThread::finished,
worker, &QObject::deleteLater);
connect(this, &Controller::startWork,
worker, &Worker::doWork);
connect(worker, &Worker::progressChanged,
this, &Controller::onProgress);
connect(worker, &Worker::workFinished,
this, &Controller::onFinished);
// 启动线程
workerThread->start();
}
~Controller()
{
workerThread->quit();
workerThread->wait();
}
void start(const QString ¶m)
{
emit startWork(param);
}
signals:
void startWork(const QString ¶meter);
private slots:
void onProgress(int percent)
{
qDebug() << "进度:" << percent << "%";
}
void onFinished(const QString &result)
{
qDebug() << "结果:" << result;
}
private:
Worker *worker;
QThread *workerThread;
};
为什么推荐 Worker-Object 模式?
- 更好的关注点分离:线程管理和业务逻辑分开
- 更灵活:一个线程可以服务多个工作对象
- 更安全:避免继承 QThread 带来的生命周期管理问题
- 更符合 Qt 的设计理念
信号与槽的线程安全
Qt 的信号槽机制天然支持跨线程通信。
连接类型
// 自动选择(默认)
connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::AutoConnection);
// 直接连接 - 槽函数在发送者线程执行
connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::DirectConnection);
// 队列连接 - 槽函数在接收者线程执行(通过事件队列)
connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::QueuedConnection);
// 阻塞队列连接 - 发送者等待槽函数执行完毕
connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::BlockingQueuedConnection);
// 唯一连接 - 避免重复连接
connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::UniqueConnection);
自动连接规则:
- 如果发送者和接收者在同一线程,使用
DirectConnection - 如果在不同线程,使用
QueuedConnection
跨线程更新 UI
// Worker.h
class Worker : public QObject
{
Q_OBJECT
public slots:
void processData(const QByteArray &data)
{
// 在工作线程处理数据
QByteArray result = heavyProcessing(data);
// 通过信号传递结果到主线程
emit resultReady(result);
}
signals:
void resultReady(const QByteArray &result);
};
// MainWindow.cpp
void MainWindow::startProcessing()
{
// 创建工作线程
QThread *thread = new QThread;
Worker *worker = new Worker;
worker->moveToThread(thread);
// 连接信号
connect(this, &MainWindow::dataReady, worker, &Worker::processData);
connect(worker, &Worker::resultReady, this, &MainWindow::handleResult);
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
thread->start();
// 发送数据到工作线程
emit dataReady(rawData);
}
void MainWindow::handleResult(const QByteArray &result)
{
// 这个槽函数在主线程执行,可以安全更新 UI
textEdit->setText(QString::fromUtf8(result));
}
线程同步
当多个线程访问共享数据时,需要使用同步机制防止数据竞争。
QMutex 互斥锁
#include <QMutex>
#include <QMutexLocker>
class ThreadSafeCounter
{
public:
void increment()
{
QMutexLocker locker(&mutex); // 自动加锁,离开作用域自动解锁
++count;
}
int get() const
{
QMutexLocker locker(&mutex);
return count;
}
private:
mutable QMutex mutex; // mutable 允许在 const 方法中使用
int count = 0;
};
QMutexLocker 的优势:
- RAII 风格,自动管理锁的生命周期
- 即使发生异常也能正确释放锁
- 代码更简洁,避免忘记解锁
QReadWriteLock 读写锁
适用于读多写少的场景:
#include <QReadWriteLock>
#include <QReadLocker>
#include <QWriteLocker>
class ThreadSafeCache
{
public:
QString get(const QString &key)
{
QReadLocker locker(&lock); // 读锁(多个读操作可以并行)
return cache.value(key);
}
void set(const QString &key, const QString &value)
{
QWriteLocker locker(&lock); // 写锁(独占访问)
cache[key] = value;
}
bool contains(const QString &key) const
{
QReadLocker locker(&lock);
return cache.contains(key);
}
private:
mutable QReadWriteLock lock;
QMap<QString, QString> cache;
};
QSemaphore 信号量
用于控制对有限资源的访问:
#include <QSemaphore>
const int bufferSize = 10;
QSemaphore freeSlots(bufferSize); // 空闲槽位
QSemaphore usedSlots(0); // 已使用槽位
QBuffer buffer;
void producer()
{
for (int i = 0; i < 100; i++) {
freeSlots.acquire(); // 等待空闲槽位
// 生产数据并放入缓冲区
buffer.put(i);
usedSlots.release(); // 增加已使用槽位
}
}
void consumer()
{
for (int i = 0; i < 100; i++) {
usedSlots.acquire(); // 等待有数据
// 从缓冲区取出数据
int data = buffer.get();
freeSlots.release(); // 增加空闲槽位
// 处理数据...
}
}
QWaitCondition 条件变量
用于线程间的等待/通知机制:
#include <QWaitCondition>
#include <QMutex>
QMutex mutex;
QWaitCondition condition;
bool ready = false;
void waitForData()
{
QMutexLocker locker(&mutex);
while (!ready) {
condition.wait(&mutex); // 释放锁并等待,被唤醒后重新获取锁
}
// 处理数据...
ready = false;
}
void setDataReady()
{
QMutexLocker locker(&mutex);
ready = true;
condition.wakeOne(); // 唤醒一个等待的线程
// 或 condition.wakeAll(); 唤醒所有等待的线程
}
Qt Concurrent 高级 API
Qt Concurrent 提供了更高级的并行编程接口,无需手动管理线程。
QtConcurrent::run
在单独线程中运行函数:
#include <QtConcurrent>
// 运行普通函数
QFuture<void> future = QtConcurrent::run([]() {
qDebug() << "在后台线程执行";
QThread::sleep(2);
});
// 运行成员函数
QFuture<QString> future = QtConcurrent::run(this, &MyClass::longRunningTask, arg1, arg2);
// 获取结果
QString result = future.result();
// 检查是否完成
if (future.isFinished()) {
// ...
}
// 等待完成
future.waitForFinished();
QtConcurrent::map 并行映射
对容器中的每个元素并行执行操作:
#include <QtConcurrent>
#include <QFutureWatcher>
void processImages()
{
QStringList imageFiles = loadImageList();
// 并行处理每个图片
QFuture<void> future = QtConcurrent::map(imageFiles, [](const QString &file) {
QImage image(file);
image = image.scaled(100, 100);
image.save("thumbnails/" + QFileInfo(file).fileName());
});
// 使用 QFutureWatcher 监控进度
QFutureWatcher<void> *watcher = new QFutureWatcher<void>(this);
connect(watcher, &QFutureWatcher<void>::finished, this, []() {
qDebug() << "所有图片处理完成";
});
connect(watcher, &QFutureWatcher<void>::progressValueChanged, this, [](int progress) {
qDebug() << "进度:" << progress;
});
watcher->setFuture(future);
}
QtConcurrent::mapped 带返回值的并行映射
QList<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 并行计算每个数的平方
QFuture<int> future = QtConcurrent::mapped(numbers, [](int n) {
return n * n;
});
// 获取所有结果
QList<int> squares = future.results();
qDebug() << squares; // {1, 4, 9, 16, 25, ...}
QtConcurrent::filter 并行过滤
QList<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 过滤出偶数
QFuture<void> future = QtConcurrent::filter(numbers, [](int n) {
return n % 2 == 0;
});
future.waitForFinished();
qDebug() << numbers; // {2, 4, 6, 8, 10}
QThreadPool 线程池
#include <QThreadPool>
#include <QRunnable>
// 定义任务
class ImageTask : public QRunnable
{
public:
ImageTask(const QString &filePath)
: m_filePath(filePath)
{
setAutoDelete(true); // 执行完毕自动删除
}
void run() override
{
QImage image(m_filePath);
// 处理图片...
emit finished(processedImage);
}
signals:
void finished(const QImage &image);
private:
QString m_filePath;
};
// 使用线程池
void processImages(const QStringList &files)
{
QThreadPool *pool = QThreadPool::globalInstance();
// 设置最大线程数
pool->setMaxThreadCount(QThread::idealThreadCount());
for (const QString &file : files) {
pool->start(new ImageTask(file));
}
// 等待所有任务完成
pool->waitForDone();
}
线程安全最佳实践
不要在工作线程直接操作 UI
// 错误做法
void Worker::run()
{
// 在工作线程中直接更新 UI - 可能导致崩溃
mainWindow->label->setText("处理中...");
}
// 正确做法
void Worker::run()
{
// 通过信号通知主线程更新 UI
emit statusChanged("处理中...");
}
使用 Qt 提供的线程安全容器
// Qt 线程安全容器
QStack<int> stack; // 线程安全栈
QQueue<int> queue; // 线程安全队列
// 或使用标准容器 + 互斥锁
std::vector<int> data;
QMutex dataMutex;
避免死锁
// 可能死锁的代码
void transfer(Amount from, Account *src, Account *dst)
{
QMutexLocker lock1(&src->mutex); // 先锁 src
QMutexLocker lock2(&dst->mutex); // 再锁 dst
// ...
}
// 如果两个线程同时执行:
// 线程1: transfer(a, b) -> 锁A,等待B
// 线程2: transfer(b, a) -> 锁B,等待A
// 结果:死锁
// 解决方案:按固定顺序加锁
void transfer(Amount from, Account *src, Account *dst)
{
// 按地址排序加锁
QMutex *first = (src < dst) ? &src->mutex : &dst->mutex;
QMutex *second = (src < dst) ? &dst->mutex : &src->mutex;
QMutexLocker lock1(first);
QMutexLocker lock2(second);
// ...
}
使用 QThread::requestInterruption
Qt 5 引入了更优雅的线程中断机制:
class InterruptibleThread : public QThread
{
protected:
void run() override
{
while (!isInterruptionRequested()) {
// 执行工作
processOneItem();
}
}
};
// 请求中断
thread->requestInterruption();
thread->wait();
完整示例:异步文件搜索
#include <QWidget>
#include <QLineEdit>
#include <QListWidget>
#include <QPushButton>
#include <QVBoxLayout>
#include <QThread>
#include <QDirIterator>
class FileSearchWorker : public QObject
{
Q_OBJECT
public:
explicit FileSearchWorker(QObject *parent = nullptr)
: QObject(parent)
{
}
public slots:
void search(const QString &directory, const QString &pattern)
{
emit started();
int count = 0;
QDirIterator it(directory, QDir::Files | QDir::NoSymLinks,
QDirIterator::Subdirectories);
while (it.hasNext() && !QThread::currentThread()->isInterruptionRequested()) {
QString filePath = it.next();
QString fileName = QFileInfo(filePath).fileName();
if (fileName.contains(pattern, Qt::CaseInsensitive)) {
emit found(filePath);
count++;
}
// 定期发送进度
if (count % 10 == 0) {
emit progress(count);
}
}
emit finished(count);
}
signals:
void started();
void found(const QString &filePath);
void progress(int count);
void finished(int total);
};
class FileSearchWidget : public QWidget
{
Q_OBJECT
public:
FileSearchWidget(QWidget *parent = nullptr) : QWidget(parent)
{
// 创建 UI
pathEdit = new QLineEdit(QDir::homePath());
patternEdit = new QLineEdit();
patternEdit->setPlaceholderText("输入文件名模式...");
searchButton = new QPushButton("搜索");
stopButton = new QPushButton("停止");
stopButton->setEnabled(false);
resultList = new QListWidget();
statusLabel = new QLabel("就绪");
// 布局
QGridLayout *grid = new QGridLayout();
grid->addWidget(new QLabel("目录:"), 0, 0);
grid->addWidget(pathEdit, 0, 1);
grid->addWidget(new QLabel("模式:"), 1, 0);
grid->addWidget(patternEdit, 1, 1);
QHBoxLayout *btnLayout = new QHBoxLayout();
btnLayout->addWidget(searchButton);
btnLayout->addWidget(stopButton);
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->addLayout(grid);
mainLayout->addLayout(btnLayout);
mainLayout->addWidget(resultList);
mainLayout->addWidget(statusLabel);
// 设置工作线程
workerThread = new QThread(this);
worker = new FileSearchWorker();
worker->moveToThread(workerThread);
// 连接信号
connect(searchButton, &QPushButton::clicked, this, &FileSearchWidget::startSearch);
connect(stopButton, &QPushButton::clicked, this, &FileSearchWidget::stopSearch);
connect(workerThread, &QThread::finished, worker, &QObject::deleteLater);
connect(this, &FileSearchWidget::searchRequested, worker, &FileSearchWorker::search);
connect(worker, &FileSearchWorker::started, this, &FileSearchWidget::onSearchStarted);
connect(worker, &FileSearchWorker::found, this, &FileSearchWidget::onFileFound);
connect(worker, &FileSearchWorker::progress, this, &FileSearchWidget::onProgress);
connect(worker, &FileSearchWorker::finished, this, &FileSearchWidget::onSearchFinished);
workerThread->start();
}
~FileSearchWidget()
{
workerThread->requestInterruption();
workerThread->quit();
workerThread->wait();
}
private slots:
void startSearch()
{
resultList->clear();
emit searchRequested(pathEdit->text(), patternEdit->text());
}
void stopSearch()
{
workerThread->requestInterruption();
}
void onSearchStarted()
{
searchButton->setEnabled(false);
stopButton->setEnabled(true);
statusLabel->setText("搜索中...");
}
void onFileFound(const QString &filePath)
{
resultList->addItem(filePath);
}
void onProgress(int count)
{
statusLabel->setText(QString("已找到 %1 个文件").arg(count));
}
void onSearchFinished(int total)
{
searchButton->setEnabled(true);
stopButton->setEnabled(false);
statusLabel->setText(QString("搜索完成,共找到 %1 个文件").arg(total));
}
signals:
void searchRequested(const QString &directory, const QString &pattern);
private:
QLineEdit *pathEdit;
QLineEdit *patternEdit;
QPushButton *searchButton;
QPushButton *stopButton;
QListWidget *resultList;
QLabel *statusLabel;
QThread *workerThread;
FileSearchWorker *worker;
};
小结
本章介绍了 Qt 多线程编程的核心内容:
- QThread 基础:继承方式和 Worker-Object 模式
- 信号槽与线程安全:跨线程通信的机制
- 线程同步:互斥锁、读写锁、信号量、条件变量
- Qt Concurrent:高级并行编程 API
- 最佳实践:避免常见错误,确保线程安全
多线程编程是复杂的,正确使用需要理解 Qt 的事件循环和信号槽机制。推荐使用 Worker-Object 模式和 Qt Concurrent,它们更安全、更易用。
练习
- 实现一个后台下载管理器,支持多个文件并行下载
- 创建一个图片处理工具,使用线程池并行处理图片
- 实现一个定时器线程,每隔一定时间发出信号
- 使用 QtConcurrent 实现一个文件夹大小计算器
- 实现一个生产者-消费者模型,使用信号量同步