在 Qt 里做耗时任务很多人第一反应是QFutureint future QtConcurrent::run([] { // 耗时操作 return 100; }); future.waitForFinished(); qDebug() future.result();代码确实简单。但如果这段代码运行在GUI 主线程那么问题就来了任务没执行完主线程就一直等。窗口不能正常刷新、按钮点击没反应、动画停止甚至直接出现“程序卡死”的感觉。这篇文章就把一个问题讲清楚QFuture 和 QFutureWatcher 到底应该怎么配合使用01.future.waitForFinished()为什么会卡界面先看一个完整例子。#include QApplication #include QPushButton #include QFuture #include QtConcurrent int main(int argc, char *argv[]) { QApplication app(argc, argv); QPushButton button(开始计算); button.resize(300, 100); button.show(); QObject::connect(button, QPushButton::clicked, []() { QFutureint future QtConcurrent::run([] { QThread::sleep(5); return 100; }); future.waitForFinished(); qDebug() result future.result(); }); return app.exec(); }点击按钮之后发生什么点击按钮 ↓ 启动后台任务 ↓ waitForFinished() ↓ 主线程等待 5 秒 ↓ GUI事件循环无法继续处理 ↓ 窗口卡住注意QtConcurrent::run()确实把计算放到了线程池。但是future.waitForFinished();又把主线程给堵住了。所以后台线程执行任务 ≠ 主线程不会阻塞。真正的问题不是QtConcurrent::run()而是你在 GUI 线程里同步等待结果。02.QFutureWatcher是干什么的这时候就轮到QFutureWatcherT登场了。它可以理解成我不等你我监听你。以前future.waitForFinished();相当于主线程 “你什么时候做完” “没做完” “那我等着。” “还没做完” “继续等……”而QFutureWatcher的思路是主线程 “你去后台慢慢算。” ↓ GUI继续运行 ↓ 后台任务完成 ↓ Watcher发出finished() ↓ 主线程收到信号 ↓ 读取结果这才是 Qt GUI 程序比较推荐的异步模式。03. 最简单的QFutureWatcher实例我们把前面的代码改掉。#include QApplication #include QPushButton #include QFuture #include QFutureWatcher #include QtConcurrent #include QThread #include QDebug int main(int argc, char *argv[]) { QApplication app(argc, argv); QPushButton button(开始计算); button.resize(300, 100); button.show(); auto *watcher new QFutureWatcherint(button); QObject::connect( button, QPushButton::clicked, []() { QFutureint future QtConcurrent::run([] { QThread::sleep(5); return 100; }); watcher-setFuture(future); }); QObject::connect( watcher, QFutureWatcherint::finished, []() { qDebug() 计算完成; int result watcher-result(); qDebug() result result; }); return app.exec(); }现在执行流程变成点击按钮 │ ▼ QtConcurrent::run() │ ▼ 线程池执行耗时任务 │ ├───────────────┐ │ │ ▼ ▼ 后台计算 GUI事件循环 │ │ │ ├── 窗口刷新 │ ├── 鼠标事件 │ └── 用户操作 │ ▼ 任务完成 │ ▼ QFutureWatcher::finished() │ ▼ 主线程处理结果最重要的一点整个过程中GUI 主线程没有调用waitForFinished()。04. 为什么result()可以直接调用很多人第一次看到connect(watcher, QFutureWatcherint::finished, []() { int result watcher-result(); });可能会疑惑result()不会阻塞吗正常情况下这里不会造成你担心的等待。因为finished()意味着 Future 对应的任务已经完成。所以watcher-result();此时就是获取已经完成的结果。逻辑可以简单理解成setFuture() ↓ 任务执行 ↓ 任务完成 ↓ finished() ↓ result()而不是result() ↓ 等待任务完成05. 一个更真实的 Qt 界面例子实际项目里我们一般不会只有一个QPushButton。比如完整代码如下。#include QApplication #include QWidget #include QPushButton #include QLabel #include QVBoxLayout #include QFuture #include QFutureWatcher #include QtConcurrent class Widget : public QWidget { public: Widget(QWidget *parent nullptr) : QWidget(parent) { auto *layout new QVBoxLayout(this); statusLabel new QLabel(等待开始); startButton new QPushButton(开始计算); layout-addWidget(statusLabel); layout-addWidget(startButton); watcher new QFutureWatcherqint64(this); connect(startButton, QPushButton::clicked, this, [this]() { startButton-setEnabled(false); statusLabel-setText(正在计算...); QFutureqint64 future QtConcurrent::run([]() - qint64 { qint64 result 0; for (qint64 i 0; i 100000000; i) { result i; } return result; }); watcher-setFuture(future); }); connect(watcher, QFutureWatcherqint64::finished, this, [this]() { qint64 result watcher-result(); statusLabel-setText( QString(计算完成结果%1) .arg(result)); startButton-setEnabled(true); }); } private: QLabel *statusLabel nullptr; QPushButton *startButton nullptr; QFutureWatcherqint64 *watcher nullptr; }; int main(int argc, char *argv[]) { QApplication app(argc, argv); Widget w; w.resize(400, 200); w.show(); return app.exec(); }这个例子就是一个比较典型的 Qt 异步任务模型。06.QFutureWatcher最大的价值状态通知QFutureWatcher并不只是监听finished()。它还可以监听任务状态。常用信号包括finished() started() canceled() paused() resumed() progressValueChanged(int) progressRangeChanged(int, int) progressTextChanged(QString)因此可以非常方便地实现耗时任务 │ ├── started │ ├── progress │ ├── progress │ ├── progress │ └── finished例如connect(watcher, QFutureWatcherint::progressValueChanged, this, [this](int value) { qDebug() progress value; });如果界面上有QProgressBar甚至可以直接connect(watcher, QFutureWatcherint::progressValueChanged, progressBar, QProgressBar::setValue);这样就形成后台任务 ↓ QFuture ↓ QFutureWatcher ↓ progressValueChanged() ↓ QProgressBar07. 一个非常容易犯的错误有人会这么写auto future QtConcurrent::run([] { // 耗时任务 }); future.waitForFinished(); watcher-setFuture(future);这就失去了QFutureWatcher的意义。因为waitForFinished();已经把任务等完了。然后才setFuture();相当于任务开始 ↓ 主线程等待 ↓ 任务完成 ↓ 主线程恢复 ↓ Watcher开始监听Watcher基本成了摆设。正确方式应该是auto future QtConcurrent::run([] { // 耗时任务 }); watcher-setFuture(future);然后connect(watcher, QFutureWatcherint::finished, this, [this]() { // 处理结果 });也就是提交任务以后立即返回不等待。08. 那waitForFinished()什么时候可以用并不是说waitForFinished()永远不能用。它只是不适合在 GUI 主线程里等待耗时任务。例如void Worker::run() { QFutureint future QtConcurrent::run([] { return doSomething(); }); future.waitForFinished(); int result future.result(); // 当前本身就在后台线程 }如果这个函数本身已经运行在专门的工作线程中那么同步等待未必有问题。真正应该避免的是GUI线程 ↓ 启动耗时任务 ↓ waitForFinished() ↓ 等待09.QFutureWatcher的生命周期也很重要推荐这样创建watcher new QFutureWatcherint(this);利用 Qt 的父子对象机制管理生命周期。然后watcher-setFuture(future);如果任务还没完成千万不要随意delete watcher;否则后台任务完成以后Watcher 相关状态通知就可能出现生命周期问题。另外一个 Watcher 通常可以重复使用watcher-setFuture(future1);任务完成后watcher-setFuture(future2);不需要每次都new QFutureWatcher10. 最终推荐的代码结构实际项目中可以形成这样的模式class DataProcessor : public QObject { public: void start() { QFutureResult future QtConcurrent::run([] { return processData(); }); watcher.setFuture(future); } private: QFutureWatcherResult watcher; };然后connect(watcher, QFutureWatcherResult::finished, this, [this]() { Result result watcher.result(); handleResult(result); });整个结构非常清晰UI │ │ start() ▼ DataProcessor │ │ QtConcurrent::run() ▼ 线程池 │ │ 耗时计算 ▼ QFuture │ ▼ QFutureWatcher │ │ finished() ▼ UI线程 │ ▼ 更新界面这就是 Qt 中非常经典的一种异步任务处理方式。11. 最后总结如果你的代码是QFutureint future QtConcurrent::run(doSomething); future.waitForFinished(); int result future.result();首先问自己一个问题这段代码是不是运行在 GUI 主线程如果是而且doSomething()可能耗时那么就应该警惕。更合适的方式通常是QFutureint future QtConcurrent::run(doSomething); watcher.setFuture(future);然后connect(watcher, QFutureWatcherint::finished, this, []() { int result watcher.result(); // 更新UI });一句话记住QFuture 负责表示异步任务QFutureWatcher 负责监听任务状态GUI 线程负责响应结果。而waitForFinished()并不是洪水猛兽。真正的问题是别在 GUI 主线程里傻等。让耗时工作去后台让主线程继续处理事件这才是 Qt GUI 程序应该有的姿势。