在 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.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% 的場景它都夠用了,而且代碼最乾淨、最安全。