原问题发生在重复点击文件项时:槽里多次查询同一个 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_OBJECTpublic: 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