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