用 QtConcurrent 实现优雅的后台任务:从零写一个“安全可取消”的长时任务示例
在 Qt 开发中,我们经常会遇到需要执行耗时操作的场景,比如文件批量处理、网络请求、复杂计算等。如果直接在主线程执行,会导致界面卡顿甚至假死。这时候最简单的解决方案就是使用 QtConcurrent —— Qt 官方提供的高级并发模块,它比手动创建 QThread 更简洁、更安全。
本文通过一个完整的可运行示例,手把手教你:
- 如何用 QtConcurrent::run 启动后台任务
- 如何安全地取消正在运行的任务
- 如何在任务结束后自动恢复 UI
- 如何重写 closeEvent 实现“窗口关闭时自动等待任务结束”
完整源码只有 100 多行,却包含了生产环境中几乎所有需要注意的细节。
最终效果演示
启动程序后,点击“开始任务”按钮:
- 状态栏每 3 秒刷新一次“后台任务正在运行...”
- 按钮文字变成“停止任务”
- 再次点击或关闭窗口 → 任务优雅停止,绝不强制终止

源码
// mainwindow.h #ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include <QFuture> // 用于保存异步任务的返回值(这里是 void) #include <QFutureWatcher> // 用于监控异步任务的状态(如完成、取消等) #include <QtConcurrent> // QtConcurrent 命名空间,提供高级并发 API(如 QtConcurrent::run) #include <atomic> // C++11 的原子变量,用于线程安全的 bool 标志 QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; // 前向声明,由 Qt Designer 生成的 UI 类 } QT_END_NAMESPACE class MainWindow : public QMainWindow { Q_OBJECT // 必须的宏,启用信号槽机制 public: explicit MainWindow(QWidget *parent = nullptr); // 构造函数 ~MainWindow(); // 析构函数 protected: void closeEvent(QCloseEvent *event) override; // 重写关闭事件 private slots: void on_btnStartStop_clicked(); // “开始/停止”按钮的点击槽函数 private: Ui::MainWindow *ui; // UI 界面指针 QFuture<void> future; // 保存后台任务的 QFuture 对象 QFutureWatcher<void> watcher; // 监控后台任务的完成状态 std::atomic<bool> running = false; // 线程安全的运行标志,控制循环是否继续 }; #endif // MAINWINDOW_H
// mainwindow.cpp #include "mainwindow.h" #include "ui_mainwindow.h" #include <QPushButton> // 动态创建按钮需要用到 #include <QMessageBox> #include <QCloseEvent> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); // 初始化 Designer 生成的 UI // 动态创建一个按钮(因为示例中没有在 .ui 文件里放按钮) QPushButton *btn = new QPushButton("开始任务", this); btn->setObjectName("btnStartStop"); // 设置对象名,方便后面 findChild 查找 btn->setGeometry(20, 20, 120, 40); // 设置位置和大小 // 连接按钮点击信号到我们自己写的槽函数 connect(btn, &QPushButton::clicked, this, &MainWindow::on_btnStartStop_clicked); // 当后台任务结束时(无论是正常结束还是被取消),自动执行以下 lambda connect(&watcher, &QFutureWatcher<void>::finished, this, [this]() { ui->statusbar->showMessage("任务已停止"); // 状态栏提示 findChild<QPushButton*>("btnStartStop")->setText("开始任务"); // 按钮文字恢复 findChild<QPushButton*>("btnStartStop")->setEnabled(true); // 重新启用按钮 }); } MainWindow::~MainWindow() { running = false; // 先通知后台线程退出循环 if (future.isRunning()) { watcher.waitForFinished(); // 等待后台任务彻底结束,避免析构时线程还在访问成员 } delete ui; } /* ==================== 重写关闭事件 ==================== */ void MainWindow::closeEvent(QCloseEvent *event) { // 如果任务没有在运行,直接允许关闭 if (!running) { event->accept(); // 正常关闭 return; } // 任务正在运行,先询问用户 QMessageBox::StandardButton reply = QMessageBox::question( this, "确认关闭", "后台任务正在运行,关闭窗口将停止任务。\n\n是否继续关闭?", QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (reply != QMessageBox::Yes) { event->ignore(); // 用户取消关闭 return; } // 用户确认关闭 → 停止任务 running = false; // 通知后台循环退出 ui->statusbar->showMessage("正在停止任务,请稍候关闭窗口..."); if (future.isRunning()) { // 禁用关闭按钮,防止用户重复点击 × setEnabled(false); // 连接一次性的槽:任务真正结束后自动关闭窗口 connect(&watcher, &QFutureWatcher<void>::finished, this, [this, event]() { // 任务已安全结束,恢复窗口可操作性并接受关闭事件 setEnabled(true); event->accept(); // 真正关闭窗口 QApplication::quit(); // 可选:彻底退出程序 }); } else { // 极少数情况下 future 已经结束,直接关闭 event->accept(); } // 重要:先 ignore,后面 finished 信号触发后再 accept event->ignore(); } /* ========================================================== */ // “开始/停止”按钮点击处理函数 void MainWindow::on_btnStartStop_clicked() { // 通过对象名找到我们动态创建的按钮 QPushButton *btn = findChild<QPushButton*>("btnStartStop"); // ---------- 1. 当前未运行 → 启动任务 ---------- if (!running) { running = true; // 设置运行标志为 true // 使用 QtConcurrent::run 在线程池中启动一个独立的线程执行 lambda future = QtConcurrent::run([this]() { // 循环体:只要 running 为 true 就一直执行 while (running) { // 因为不能在子线程直接操作 UI,必须投递到主线程 QMetaObject::invokeMethod(this, [this](){ ui->statusbar->showMessage("后台任务正在运行..."); }, Qt::QueuedConnection); // 模拟耗时工作,每 3 秒执行一次 QThread::sleep(3); } }); // 把 future 交给 watcher 管理,这样才能收到 finished 信号 watcher.setFuture(future); ui->statusbar->showMessage("任务已启动"); btn->setText("停止任务"); // 按钮文字改为“停止任务” return; } // ---------- 2. 当前正在运行 → 停止任务 ---------- running = false; // 通知后台线程退出 while 循环 btn->setEnabled(false); // 禁用按钮,防止用户连续点击导致多次停止逻辑 ui->statusbar->showMessage("正在停止,请稍候..."); // 实际停止完成后,watcher 的 finished 信号会触发构造函数里连接的 lambda, // 自动恢复按钮文字和启用状态 }
核心代码解析
- std::atomic<bool> 是线程安全的首选标志 C++11 引入的原子变量,在多线程环境下读写天然安全,无需额外加 mutex 或使用容易出错的 volatile,推荐所有可取消任务都用它来控制循环。
- QFutureWatcher 是任务生命周期的“哨兵” 只要把 QFuture 交给 watcher.setFuture(future),无论任务是正常返回还是因为 running=false 自然退出循环,watcher.finished 信号必定会触发一次,非常适合用来统一恢复按钮、状态栏等 UI。
- 原子变量的可见性保证 由于 running 是 std::atomic,主线程一执行 running = false;,子线程在下一次 while 判断时立即就能看到新值,实现“秒级响应取消”而不需要轮询或中断信号。
- QtConcurrent::run 自动使用 QThreadPool 不需要手动创建、删除或 moveToThread,Qt 会自动复用线程池线程,资源利用率高,代码量极少,属于“开箱即用”的最高级并发 API。
- finished 信号的“无论如何都会发”特性 即使任务是通过 while(running) 自然退出(没有抛异常、没有调用 cancel()),QFutureWatcher::finished 依然会可靠发射,这是和普通 QThread 最大的区别之一——你永远可以只连接这一个信号来做“收尾工作”。
一句话总结: QtConcurrent::run + std::atomic<bool> + QFutureWatcher::finished = 最少代码、最安全、最优雅的可取消长时后台任务方案,强烈推荐在实际项目中作为首选模板。
总结:为什么推荐 QtConcurrent?
| 方案 | 代码量 | 学习成本 | 取消难度 | 线程管理 | 推荐度 |
|---|---|---|---|---|---|
| 手动 QThread | 多 | 高 | 中 | 手动 | ★★★☆☆ |
| QThread + moveToThread | 中 | 中 | 中 | 手动 | ★★★★☆ |
| QtConcurrent | 少 | 低 | 简单 | 自动 | ★★★★★ |
QtConcurrent 的最大优势:
- 只需要一行 QtConcurrent::run 就能启动线程池任务
- 配合 std::atomic + QFutureWatcher 就能实现完美的可取消长时任务
- 完全不需要自己写 QThread 子类或处理 moveToThread
如果你正在写一个需要后台任务的 Qt 程序,强烈建议从 QtConcurrent 开始——90% 的场景它都够用了,而且代码最干净、最安全。

浙公网安备 33010602011771号