用 QtConcurrent 实现优雅的后台任务:从零写一个“安全可取消”的长时任务示例

在 Qt 开发中,我们经常会遇到需要执行耗时操作的场景,比如文件批量处理、网络请求、复杂计算等。如果直接在主线程执行,会导致界面卡顿甚至假死。这时候最简单的解决方案就是使用 QtConcurrent —— Qt 官方提供的高级并发模块,它比手动创建 QThread 更简洁、更安全。

本文通过一个完整的可运行示例,手把手教你:

  • 如何用 QtConcurrent::run 启动后台任务
  • 如何安全地取消正在运行的任务
  • 如何在任务结束后自动恢复 UI
  • 如何重写 closeEvent 实现“窗口关闭时自动等待任务结束”

完整源码只有 100 多行,却包含了生产环境中几乎所有需要注意的细节。

最终效果演示

启动程序后,点击“开始任务”按钮:

  • 状态栏每 3 秒刷新一次“后台任务正在运行...”
  • 按钮文字变成“停止任务”
  • 再次点击或关闭窗口 → 任务优雅停止,绝不强制终止

ScreenGif

 

源码

// 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.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,
    // 自动恢复按钮文字和启用状态
}
mainwindow.cpp

核心代码解析

  • 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% 的场景它都够用了,而且代码最干净、最安全。

 

posted @ 2025-11-18 14:49  阿坦  阅读(51)  评论(0)    收藏  举报