Qt 异步任务与对象生命周期的"隐形地雷"
QFutureWatcher 与对象生命周期
窗口关闭时 QFutureWatcher 收不到 finished 信号的两种根治方案
完整代码
cpp
class SubWidget : public QMainWindow
{
Q_OBJECT
public:
SubWidget(QWidget *parent = nullptr);
~SubWidget();
protected:
void closeEvent(QCloseEvent* event) override;
private slots:
void onQFutureWatcherFinished();
private:
QFutureWatcher<bool>* createWatcher();
private:
Ui::SubWidgetClass ui;
QPointer<QFutureWatcher<bool>> m_pWatcher;
};
cpp
#include <QDebug>
#include <QFuture>
#include <QThread>
#include <QtConcurrent/QtConcurrent>
SubWidget::SubWidget(QWidget *parent)
: QMainWindow(parent)
{
ui.setupUi(this);
}
SubWidget::~SubWidget()
{
/*
如果testAttribute(Qt::WA_DeleteOnClose) == true,
就等同于在析构函数中调用了m_pWatcher = createWatcher();
*/
qDebug() << "SubWidget::~SubWidget: destructor called";
int debug = 0;
}
void SubWidget::closeEvent(QCloseEvent* event)
{
QMainWindow::closeEvent(event);
createWatcher();
/*
如果testAttribute(Qt::WA_DeleteOnClose) == true,
就等同于在析构函数中调用了m_pWatcher = createWatcher();
*/
//
#if 解决方案一
m_pWatcher = createWatcher();
if (m_pWatcher != nullptr && m_pWatcher->isRunning())
{
qDebug() << "SubWidget::closeEvent: m_pWatcher is running, waiting for it to finish";
m_pWatcher->waitForFinished();
qDebug() << "SubWidget::closeEvent: waiting for it to finished";
int debug = 0;
}
#endif
}
void SubWidget::onQFutureWatcherFinished()
{
qDebug() << "QObject::sender():" << QObject::sender()->objectName();
auto pWatcher = dynamic_cast<QFutureWatcher<bool> *>(sender());
if (pWatcher == nullptr)
{
qDebug() << "onQFutureWatcherFinished: pWatcher is nullptr";
return;
}
qDebug() << "onQFutureWatcherFinished: pWatcher is not nullptr";
pWatcher->deleteLater();
}
QFutureWatcher<bool>* SubWidget::createWatcher()
{
QFutureWatcher<bool>* pWatcher = new QFutureWatcher<bool>;
pWatcher->setObjectName(QString("pWatcher_SubWidget_%1").arg(this->testAttribute(Qt::WA_DeleteOnClose)));
connect(pWatcher, &QFutureWatcher<bool>::finished, this, &SubWidget::onQFutureWatcherFinished);
//解决方案二
#if 1
connect(pWatcher, &QFutureWatcher<bool>::finished, [pWatcher]() {
qDebug() << "QFutureWatcher finished";
if (pWatcher == nullptr)
{
qDebug() << "QFutureWatcher is nullptr";
return;
}
qDebug() << "QObject::sender():" << pWatcher->objectName();
auto re = pWatcher->result();
int debug = 0;
});
#endif
pWatcher->setFuture(QtConcurrent::run([=]() -> bool {
qDebug() << "Running in a separate thread";
QThread::sleep(3);
return true;
}));
return pWatcher;
}
使用
cpp
auto pSubWidget = new SubWidget(this);
pSubWidget->setAttribute(Qt::WA_DeleteOnClose, true);
pSubWidget->show();
现象回放
1.为什么在 closeEvent 里启动任务却永远等不到 finished?
Qt::WA_DeleteOnClose
为 true,析构后槽不被调用。
尝试在窗口关闭时做异步清理:
cpp
void SubWidget::closeEvent(QCloseEvent* e)
{
QMainWindow::closeEvent(e);
createWatcher(); // 启动 QtConcurrent::run
}
运行结果:
- 控制台只打印
cpp
Running in a separate thread
SubWidget::~SubWidget: destructor called
- 永远收不到
cpp
onQFutureWatcherFinished ...
根本原因:
createWatcher()
产生的QFutureWatcher
没有父对象,也没有任何智能指针托管。- 当
closeEvent
返回后,事件循环继续,SubWidget
可能立即被deleteLater
析构。 - 如果
SubWidget
先析构,而线程 3 秒后才发射finished
,信号会投递到一块已释放的内存。 SubWidget
析构后,所有以this
为接收者的connect
自动断开;finished
信号再也找不到对象,于是消失在空气中。
2.方案总览
方案 | 思路 | 是否阻塞 UI | 是否安全 | 适用场景 |
---|---|---|---|---|
方案一:阻塞等待 | 在 closeEvent 里 waitForFinished() |
是 | 安全(但卡 UI) | 非常简单的清理工作,用户可接受假死 |
方案二:异步自毁 | 让 QFutureWatcher 自己 deleteLater() ,不再依赖 this |
否 | 安全 | 真正异步,用户体验好 |
3.方案一:阻塞等待(waitForFinished)
3.1 代码
cpp
void SubWidget::closeEvent(QCloseEvent* e)
{
QMainWindow::closeEvent(e);
m_pWatcher = createWatcher(); // createWatcher 见题面
if (m_pWatcher && m_pWatcher->isRunning())
{
qDebug() << "waiting...";
m_pWatcher->waitForFinished(); // 阻塞
qDebug() << "wait done";
}
}
3.2 要点
waitForFinished()
会阻塞当前线程(通常是 GUI 线程),界面会卡住 3 秒。- 因为阻塞期间
SubWidget
对象仍在,所以槽函数可以正常调用,不会出现野指针。 - 不需要额外的
deleteLater()
,函数结束后m_pWatcher
作为普通局部变量被销毁即可。
3.3 优缺点
- ✅ 实现简单、无生命周期坑
- ❌ UI 假死;如果任务很长,体验极差
4.方案二:异步自毁(Lambda + deleteLater)
4.1 核心思想
- 把
QFutureWatcher
的生命周期与SubWidget
解耦。 - 用捕获列表
[=]
把pWatcher
拷进 lambda,不再使用this
。 - 任务结束时自己
deleteLater()
,彻底避免野指针。
4.2 代码(简化后)
cpp
QFutureWatcher<bool>* SubWidget::createWatcher()
{
auto* watcher = new QFutureWatcher<bool>; // 无父对象
watcher->setObjectName("watcher_async");
// 关键:不连接到 this,而是连接到 lambda
connect(watcher, &QFutureWatcher<bool>::finished,
[watcher] {
qDebug() << "finished, result =" << watcher->result();
watcher->deleteLater();
});
watcher->setFuture(QtConcurrent::run([] {
qDebug() << "Running in a separate thread";
QThread::sleep(3);
return true;
}));
return watcher; // 调用者可立即返回,无需等待
}
调用方:
cpp
void SubWidget::closeEvent(QCloseEvent* e)
{
QMainWindow::closeEvent(e);
createWatcher(); // 立即返回,不阻塞
}
4.3 要点
- 不依赖
this
:即使SubWidget
马上析构,lambda 仍持有watcher
的拷贝,不会变成悬垂连接。 - 自动销毁:
deleteLater()
把销毁动作排入事件循环,线程结束后回到 GUI 线程时安全析构。 - 无内存泄漏:只要
finished
信号被发射一次,就必然触发deleteLater()
。 - UI 不卡顿:完全符合"异步任务"初衷。
- 两种方案对比总结
维度 | 方案一:阻塞等待 | 方案二:异步自毁 |
---|---|---|
是否卡 UI | 是 | 否 |
代码复杂度 | 低 | 中 |
生命周期安全 | 高 | 高(需遵循不访问 this ) |
适用场景 | 快速退出、简单清理 | 耗时任务、优雅关闭 |
Qt 官方推荐 | ❌ | ✅ |
-
常见踩坑清单
-
在析构函数里
new
一个对象却不delete
→ 内存泄漏或野指针。
-
把
finished
连接到this
槽,但对象先走→ 信号投递到僵尸对象。
-
在
destroyed
信号里再deleteLater(sender())
→ 重复释放,直接崩溃。
-
以为
createWatcher()
返回的裸指针会自动管理→ Qt 只有
QObject
树才会自动析构,普通裸指针不会。
- 一句话结论
- 短任务、可接受假死 → 用方案一
waitForFinished()
,简单粗暴。 - 长任务、要求流畅 → 用方案二 Lambda + deleteLater,彻底摆脱对象生命周期噩梦。