跳转到内容
新建笔记

Qt 异步任务:完成通知、重复请求与对象寿命

原问题发生在重复点击文件项时:槽里多次查询同一个 QFuture<void>::isFinished(),输出先是 false,随后变成 true;增删 qDebug() 又会改变分支是否执行,之后出现 pure virtual method called。这里应把任务状态、请求是否仍有效和崩溃原因分开调查。

QFuture 可以按值复制,副本共享异步计算的状态;原 QFuture 对象析构,不会使仍存活的副本自动悬空。把参数改成 const QFuture<void>& 也不会凭空延长引用目标或后台任务依赖对象的寿命。

两次查询间任务可能刚好完成,所以 false → true 是正常状态变化。日志输出改变时间安排,能影响哪个查询先于完成发生,但不能据此证明悬空引用,也不能证明日志修好了程序。QFuture 析构不会自动等待或停止计算;后台函数捕获的原始指针、引用和 this 仍需独立管理。QFuture

原日志中的自动槽连接警告应检查槽名、控件 objectName 和信号签名;pure virtual method called 应在触发点取得调用栈,调查对象析构期间/之后的虚调用、悬空对象及跨线程访问。仅凭这几行日志,不能确定崩溃对象或根因。普通指针非空不证明对象仍活着,sender() 也不能用于检查接收者存活。

用完成通知处理当前请求

跳转到“用完成通知处理当前请求”

点击时只检查一次 isFinished(),在尚未完成的分支中什么都不做,会丢掉后续操作。用 QFutureWatcher 连接 finished,并在 setFuture() 前建立连接;已经完成的 future 也可以交给 watcher。QFutureWatcher

下面是 Qt 6、C++17 的完整程序,已在 Qt 6.10.2 编译运行。它用整数表示加载结果,演示“只接受最后一次选择”:新请求使旧请求失效,模型行被删除或模型重置后不使用旧索引,接收对象销毁后不再回调。QPromise 让验证程序准确控制每个任务何时完成,不依赖睡眠或日志时延。

// main.cpp;链接 Qt6::Core、Qt6::Gui,并启用 AUTOMOC
#include <QCoreApplication>
#include <QEvent>
#include <QFuture>
#include <QFutureWatcher>
#include <QPersistentModelIndex>
#include <QPointer>
#include <QPromise>
#include <QStandardItemModel>
#include <QString>
#include <QThread>
#include <cassert>
#include <exception>
#include <optional>
#include <stdexcept>
class LoadCoordinator final : public QObject {
Q_OBJECT
public:
using QObject::QObject;
void watch(const QModelIndex &index, QFuture<int> future) {
Q_ASSERT(QThread::currentThread() == thread());
const auto request = ++generation_;
if (watcher_) {
disconnect(watcher_.data(), nullptr, this, nullptr);
watcher_->deleteLater();
}
auto *watcher = new QFutureWatcher<int>(this);
watcher_ = watcher;
const QPointer<QFutureWatcher<int>> guard(watcher);
const QPersistentModelIndex persistent(index);
connect(watcher, &QFutureWatcher<int>::finished, this,
[this, guard, persistent, request] {
if (!guard || request != generation_ || watcher_ != guard) return;
QFuture<int> done = guard->future();
watcher_ = nullptr;
guard->deleteLater();
if (!persistent.isValid()) return;
std::optional<int> value;
try {
// finished 已到达;此时不会等任务,仍可重新抛出异常。
done.waitForFinished();
if (done.isCanceled()) return;
if (done.resultCount() == 0) {
emit failed(QStringLiteral("任务未提供结果"));
return;
}
value = done.result();
} catch (...) {
emit failed(QStringLiteral("任务失败"));
return;
}
emit ready(persistent, *value);
});
watcher->setFuture(future);
}
signals:
void ready(QPersistentModelIndex index, int value);
void failed(QString message);
private:
quint64 generation_ = 0;
QPointer<QFutureWatcher<int>> watcher_;
};
// 仅用于本验证:派发已入队的通知和延迟删除。
void pump() {
QCoreApplication::sendPostedEvents();
QCoreApplication::processEvents();
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
}
int main(int argc, char *argv[]) {
QCoreApplication app(argc, argv);
QStandardItemModel model(2, 1);
LoadCoordinator loader;
int deliveries = 0;
int errors = 0;
QObject::connect(&loader, &LoadCoordinator::ready, &app,
[&](QPersistentModelIndex index, int value) {
assert(index.isValid());
assert(value == 22);
++deliveries;
});
QObject::connect(&loader, &LoadCoordinator::failed, &app,
[&](const QString &) { ++errors; });
QPromise<int> oldTask, newTask;
oldTask.start();
newTask.start();
QFuture<int> copy;
{ const auto original = oldTask.future(); copy = original; }
assert(!copy.isFinished()); // original 已析构,副本仍有效
loader.watch(model.index(0, 0), copy);
loader.watch(model.index(1, 0), newTask.future());
newTask.addResult(22);
newTask.finish();
pump();
assert(deliveries == 1);
oldTask.addResult(11);
oldTask.finish();
pump();
assert(copy.isFinished() && copy.result() == 11);
assert(deliveries == 1); // 旧请求晚完成,不覆盖新结果
QPromise<int> resetTask;
resetTask.start();
loader.watch(model.index(0, 0), resetTask.future());
model.clear();
resetTask.addResult(33);
resetTask.finish();
pump();
assert(deliveries == 1);
model.setRowCount(1);
model.setColumnCount(1);
QPromise<int> closingTask;
closingTask.start();
auto *temporary = new LoadCoordinator;
QObject::connect(temporary, &LoadCoordinator::ready, &app,
[&](QPersistentModelIndex, int) { ++deliveries; });
temporary->watch(model.index(0, 0), closingTask.future());
delete temporary;
closingTask.addResult(44);
closingTask.finish();
pump();
assert(deliveries == 1);
assert(errors == 0);
QPromise<int> failedTask;
failedTask.start();
loader.watch(model.index(0, 0), failedTask.future());
failedTask.setException(std::make_exception_ptr(std::runtime_error("failed")));
failedTask.finish();
pump();
assert(errors == 1 && deliveries == 1);
QPromise<int> canceledTask;
canceledTask.start();
auto canceled = canceledTask.future();
loader.watch(model.index(0, 0), canceled);
canceled.cancel();
canceledTask.finish();
pump();
assert(errors == 1 && deliveries == 1);
}
#include "main.moc"

这里的 this 是 connect(sender, signal, context, functor) 的第三个实参,表示接收上下文;整个调用若加连接类型,它才是第五个实参。上下文销毁时连接会解除。Lambda 捕获里的其他 QObject 或普通指针仍需各自的寿命契约,不能把自动断连理解成任意捕获都安全。QObject 连接说明

deleteLater() 是排队删除,并非立即断开旧连接,所以例子先断开旧 watcher 到当前对象的连接,再用请求代号与当前 watcher 检查拦截过期通知。QPointer 跟踪 QObject 销毁,不能把它当成跨线程访问的锁;本例所有 watch、模型访问和回调都在同一线程。

持久索引可跟随受支持的模型结构变化,删除对应项、重置或销毁模型会使它失效;它不使模型本身支持跨线程访问。实际文件加载只向后台传递独立的路径或数据快照,完成后再在界面线程核对请求和索引。QPersistentModelIndex

接入真实任务时的边界

跳转到“接入真实任务时的边界”

将示例中的 QPromise 替换为 QtConcurrent 返回的 future 即可沿用通知结构。若仍用 QFuture<void>,它只能表达完成状态,文件是否成功加载应另有明确的结果/错误通道;不能把 isFinished() 或 isValid() 当作加载成功判断。

忽略旧结果不等于停止旧计算。basic 模式的 QtConcurrent::run 不能靠 cancel() 强制打断正在运行的函数;需要任务自身实现协作停止,或者使用支持取消的 promise 模式。窗口销毁时,后台函数也不得继续解引用已销毁的窗口。不要为了等任务完成而在正常 GUI 操作里阻塞事件循环。QtConcurrent::run