QFuture: Gracefully handle a destroyed context in continuations

This patch relaxes the requirements on the context object of
continuations. Instead of having to stay alive during execution of the
whole chain, it now only has to stay alive during setup of the chain.
If the context object is destroyed before the chain finishes, the
respective future is canceled.

This patch works by using QFutureCallOutInterface and signals instead
of direct invocation of the continuation by the parent future, similar
to how QFutureWatcher is implemented.
If a continuation is used with a context object, a QBasicFutureWatcher
is connected to the QFuture via a QFutureCallOutInterface. When the
future finishes, QBasicFutureWatcher::finished() triggers the
continuation with a signal/slot connection.
This way, we require the context object to stay alive only during setup;
the required synchronization is guaranteed by the existing event and
signal-slot mechanisms. The continuation itself does not need to know
about the context object anymore.

[ChangeLog][QtCore][QFuture] Added support for context objects of
continuations being destroyed before the continuation finishes. In
these cases the future is cancelled immediately.

Fixes: QTBUG-112958
Change-Id: Ie0ef3470b2a0ccfa789d2ae7604b92e509c14591
Reviewed-by: Ivan Solovev <ivan.solovev@qt.io>
This commit is contained in:
Arno Rehn 2023-05-08 15:28:11 +02:00
parent 86c044176f
commit 07d6d31a4c
9 changed files with 274 additions and 62 deletions

View File

@ -689,6 +689,7 @@ qt_internal_extend_target(Core CONDITION QT_FEATURE_thread AND UNIX AND NOT APPL
qt_internal_extend_target(Core CONDITION QT_FEATURE_future
SOURCES
thread/qexception.cpp thread/qexception.h
thread/qbasicfuturewatcher.cpp thread/qbasicfuturewatcher.h
thread/qfuture.h
thread/qfuture_impl.h
thread/qfutureinterface.cpp thread/qfutureinterface.h thread/qfutureinterface_p.h

View File

@ -427,3 +427,16 @@ const bool started = f.isStarted(); // started == true
const bool running = f.isRunning(); // running == false
const bool finished = f.isFinished(); // finished == true
//! [36]
//! [37]
QObject *context = ...;
auto future = ...;
auto continuation = future.then(context, [context](Result result) {
// ...
}).onCanceled([context = QPointer(context)] {
if (!context)
return; // context was destroyed already
// handle cancellation
});
//! [37]

View File

@ -0,0 +1,80 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qbasicfuturewatcher.h"
#include "qcoreapplication.h"
#include "qfutureinterface.h"
#include "qfutureinterface_p.h"
#include <QtCore/private/qobject_p.h>
QT_BEGIN_NAMESPACE
namespace QtPrivate {
class QBasicFutureWatcherPrivate : public QObjectPrivate, QFutureCallOutInterface
{
public:
Q_DECLARE_PUBLIC(QBasicFutureWatcher)
QFutureInterfaceBase future;
void postCallOutEvent(const QFutureCallOutEvent &event) override;
void callOutInterfaceDisconnected() override;
};
void QBasicFutureWatcherPrivate::postCallOutEvent(const QFutureCallOutEvent &event)
{
Q_Q(QBasicFutureWatcher);
if (q->thread() == QThread::currentThread()) {
// If we are in the same thread, don't queue up anything.
std::unique_ptr<QFutureCallOutEvent> clonedEvent(event.clone());
QCoreApplication::sendEvent(q, clonedEvent.get());
} else {
QCoreApplication::postEvent(q, event.clone());
}
}
void QBasicFutureWatcherPrivate::callOutInterfaceDisconnected()
{
Q_Q(QBasicFutureWatcher);
QCoreApplication::removePostedEvents(q, QEvent::FutureCallOut);
}
/*
* QBasicFutureWatcher is a more lightweight version of QFutureWatcher for internal use
*/
QBasicFutureWatcher::QBasicFutureWatcher(QObject *parent)
: QObject(*new QBasicFutureWatcherPrivate, parent)
{
}
QBasicFutureWatcher::~QBasicFutureWatcher()
{
Q_D(QBasicFutureWatcher);
d->future.d->disconnectOutputInterface(d);
}
void QBasicFutureWatcher::setFuture(QFutureInterfaceBase &fi)
{
Q_D(QBasicFutureWatcher);
d->future = fi;
d->future.d->connectOutputInterface(d);
}
bool QtPrivate::QBasicFutureWatcher::event(QEvent *event)
{
if (event->type() == QEvent::FutureCallOut) {
QFutureCallOutEvent *callOutEvent = static_cast<QFutureCallOutEvent *>(event);
if (callOutEvent->callOutType == QFutureCallOutEvent::Finished)
emit finished();
return true;
}
return QObject::event(event);
}
} // namespace QtPrivate
QT_END_NAMESPACE
#include "moc_qbasicfuturewatcher.cpp"

View File

@ -0,0 +1,39 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#ifndef QBASICFUTUREWATCHER_H
#define QBASICFUTUREWATCHER_H
#include <QtCore/qobject.h>
QT_REQUIRE_CONFIG(future);
QT_BEGIN_NAMESPACE
class QFutureInterfaceBase;
namespace QtPrivate {
class QBasicFutureWatcherPrivate;
class Q_CORE_EXPORT QBasicFutureWatcher : public QObject
{
Q_OBJECT
Q_DECLARE_PRIVATE(QBasicFutureWatcher)
public:
explicit QBasicFutureWatcher(QObject *parent = nullptr);
~QBasicFutureWatcher() override;
void setFuture(QFutureInterfaceBase &fi);
bool event(QEvent *event) override;
Q_SIGNALS:
void finished();
};
}
QT_END_NAMESPACE
#endif // QBASICFUTUREWATCHER_H

View File

@ -1298,8 +1298,18 @@
is attached. In this case it will be resolved in the current thread. Therefore, when
in doubt, pass the context explicitly.
\target context_lifetime
If the \a context is destroyed before the chain has finished, the future is canceled.
This implies that a cancellation handler might be invoked when the \a context is not valid
anymore. To guard against this, capture the \a context as a QPointer:
\snippet code/src_corelib_thread_qfuture.cpp 37
When the context object is destroyed, cancellation happens immediately. Previous futures in the
chain are \e {not} cancelled and keep running until they are finished.
\note When calling this method, it should be guaranteed that the \a context stays alive
throughout the execution of the chain.
during setup of the chain.
\sa onFailed(), onCanceled()
*/
@ -1364,8 +1374,11 @@
be invoked from a non-gui thread. So \c this is provided as a context to \c .onFailed(),
to make sure that it will be invoked in the main thread.
If the \a context is destroyed before the chain has finished, the future is canceled.
See \l {context_lifetime}{then()} for details.
\note When calling this method, it should be guaranteed that the \a context stays alive
throughout the execution of the chain.
during setup of the chain.
See the documentation of the other overload for more details about \a handler.
@ -1427,8 +1440,11 @@
invoked in the thread of the \a context object. This can be useful if the cancellation
needs to be handled in a specific thread.
If the \a context is destroyed before the chain has finished, the future is canceled.
See \l {context_lifetime}{then()} for details.
\note When calling this method, it should be guaranteed that the \a context stays alive
throughout the execution of the chain.
during setup of the chain.
See the documentation of the other overload for more details about \a handler.

View File

@ -11,6 +11,7 @@
#endif
#include <QtCore/qglobal.h>
#include <QtCore/qbasicfuturewatcher.h>
#include <QtCore/qfutureinterface.h>
#include <QtCore/qthreadpool.h>
#include <QtCore/qexception.h>
@ -597,21 +598,27 @@ void Continuation<Function, ResultType, ParentResultType>::create(F &&func,
QObject *context)
{
Q_ASSERT(f);
Q_ASSERT(context);
auto continuation = [func = std::forward<F>(func), fi,
context = QPointer<QObject>(context)](
const QFutureInterfaceBase &parentData) mutable {
Q_ASSERT(context);
const auto parent = QFutureInterface<ParentResultType>(parentData).future();
QMetaObject::invokeMethod(
context,
[func = std::forward<F>(func), promise = QPromise(fi), parent]() mutable {
SyncContinuation<Function, ResultType, ParentResultType> continuationJob(
std::forward<Function>(func), parent, std::move(promise));
continuationJob.execute();
});
// When the context object is destroyed, the signal-slot connection is broken and the
// continuation callback is destroyed. The promise that is created in the capture list is
// destroyed and, if it is not yet finished, cancelled.
auto continuation = [func = std::forward<F>(func), parent = *f,
promise = QPromise(fi)]() mutable {
SyncContinuation<Function, ResultType, ParentResultType> continuationJob(
std::forward<Function>(func), parent, std::move(promise));
continuationJob.execute();
};
f->d.setContinuation(ContinuationWrapper(std::move(continuation)), fi.d);
auto *watcher = new QBasicFutureWatcher;
watcher->moveToThread(context->thread());
QObject::connect(watcher, &QBasicFutureWatcher::finished,
context, std::move(continuation));
QObject::connect(watcher, &QBasicFutureWatcher::finished,
watcher, &QObject::deleteLater);
QObject::connect(context, &QObject::destroyed,
watcher, &QObject::deleteLater);
watcher->setFuture(f->d);
}
template<typename Function, typename ResultType, typename ParentResultType>
@ -695,22 +702,20 @@ void FailureHandler<Function, ResultType>::create(F &&function, QFuture<ResultTy
QObject *context)
{
Q_ASSERT(future);
Q_ASSERT(context);
auto failureContinuation = [function = std::forward<F>(function),
parent = *future, promise = QPromise(fi)]() mutable {
FailureHandler<Function, ResultType> failureHandler(
std::forward<Function>(function), parent, std::move(promise));
failureHandler.run();
};
auto failureContinuation =
[function = std::forward<F>(function), fi,
context = QPointer<QObject>(context)](const QFutureInterfaceBase &parentData) mutable {
Q_ASSERT(context);
const auto parent = QFutureInterface<ResultType>(parentData).future();
QMetaObject::invokeMethod(context,
[function = std::forward<F>(function),
promise = QPromise(fi), parent]() mutable {
FailureHandler<Function, ResultType> failureHandler(
std::forward<Function>(function), parent, std::move(promise));
failureHandler.run();
});
};
future->d.setContinuation(ContinuationWrapper(std::move(failureContinuation)));
auto *watcher = new QBasicFutureWatcher;
watcher->moveToThread(context->thread());
QObject::connect(watcher, &QBasicFutureWatcher::finished, context, std::move(failureContinuation));
QObject::connect(watcher, &QBasicFutureWatcher::finished, watcher, &QObject::deleteLater);
QObject::connect(context, &QObject::destroyed, watcher, &QObject::deleteLater);
watcher->setFuture(future->d);
}
template<class Function, class ResultType>
@ -796,19 +801,19 @@ public:
QObject *context)
{
Q_ASSERT(future);
auto canceledContinuation = [fi, handler = std::forward<F>(handler),
context = QPointer<QObject>(context)](
const QFutureInterfaceBase &parentData) mutable {
Q_ASSERT(context);
auto parentFuture = QFutureInterface<ResultType>(parentData).future();
QMetaObject::invokeMethod(context,
[promise = QPromise(fi), parentFuture,
handler = std::forward<F>(handler)]() mutable {
run(std::forward<F>(handler), parentFuture, std::move(promise));
});
Q_ASSERT(context);
auto canceledContinuation = [handler = std::forward<F>(handler),
parentFuture = *future, promise = QPromise(fi)]() mutable {
run(std::forward<F>(handler), parentFuture, std::move(promise));
};
future->d.setContinuation(ContinuationWrapper(std::move(canceledContinuation)));
auto *watcher = new QBasicFutureWatcher;
watcher->moveToThread(context->thread());
QObject::connect(watcher, &QBasicFutureWatcher::finished,
context, std::move(canceledContinuation));
QObject::connect(watcher, &QBasicFutureWatcher::finished, watcher, &QObject::deleteLater);
QObject::connect(context, &QObject::destroyed, watcher, &QObject::deleteLater);
watcher->setFuture(future->d);
}
template<class F = Function>

View File

@ -7,6 +7,7 @@
#include <QtCore/qatomic.h>
#include <QtCore/qthread.h>
#include <QtCore/qvarlengtharray.h>
#include <QtCore/private/qsimd_p.h> // for qYieldCpu()
#include <private/qthreadpool_p.h>
@ -751,23 +752,25 @@ void QFutureInterfaceBasePrivate::connectOutputInterface(QFutureCallOutInterface
{
QMutexLocker locker(&m_mutex);
QVarLengthArray<std::unique_ptr<QFutureCallOutEvent>, 3> events;
const auto currentState = state.loadRelaxed();
if (currentState & QFutureInterfaceBase::Started) {
interface->postCallOutEvent(QFutureCallOutEvent(QFutureCallOutEvent::Started));
events.emplace_back(new QFutureCallOutEvent(QFutureCallOutEvent::Started));
if (m_progress) {
interface->postCallOutEvent(QFutureCallOutEvent(QFutureCallOutEvent::ProgressRange,
m_progress->minimum,
m_progress->maximum));
interface->postCallOutEvent(QFutureCallOutEvent(QFutureCallOutEvent::Progress,
m_progressValue,
m_progress->text));
events.emplace_back(new QFutureCallOutEvent(QFutureCallOutEvent::ProgressRange,
m_progress->minimum,
m_progress->maximum));
events.emplace_back(new QFutureCallOutEvent(QFutureCallOutEvent::Progress,
m_progressValue,
m_progress->text));
} else {
interface->postCallOutEvent(QFutureCallOutEvent(QFutureCallOutEvent::ProgressRange,
0,
0));
interface->postCallOutEvent(QFutureCallOutEvent(QFutureCallOutEvent::Progress,
m_progressValue,
QString()));
events.emplace_back(new QFutureCallOutEvent(QFutureCallOutEvent::ProgressRange,
0,
0));
events.emplace_back(new QFutureCallOutEvent(QFutureCallOutEvent::Progress,
m_progressValue,
QString()));
}
}
@ -776,25 +779,29 @@ void QFutureInterfaceBasePrivate::connectOutputInterface(QFutureCallOutInterface
while (it != data.m_results.end()) {
const int begin = it.resultIndex();
const int end = begin + it.batchSize();
interface->postCallOutEvent(QFutureCallOutEvent(QFutureCallOutEvent::ResultsReady,
begin,
end));
events.emplace_back(new QFutureCallOutEvent(QFutureCallOutEvent::ResultsReady,
begin,
end));
it.batchedAdvance();
}
}
if (currentState & QFutureInterfaceBase::Suspended)
interface->postCallOutEvent(QFutureCallOutEvent(QFutureCallOutEvent::Suspended));
events.emplace_back(new QFutureCallOutEvent(QFutureCallOutEvent::Suspended));
else if (currentState & QFutureInterfaceBase::Suspending)
interface->postCallOutEvent(QFutureCallOutEvent(QFutureCallOutEvent::Suspending));
events.emplace_back(new QFutureCallOutEvent(QFutureCallOutEvent::Suspending));
if (currentState & QFutureInterfaceBase::Canceled)
interface->postCallOutEvent(QFutureCallOutEvent(QFutureCallOutEvent::Canceled));
events.emplace_back(new QFutureCallOutEvent(QFutureCallOutEvent::Canceled));
if (currentState & QFutureInterfaceBase::Finished)
interface->postCallOutEvent(QFutureCallOutEvent(QFutureCallOutEvent::Finished));
events.emplace_back(new QFutureCallOutEvent(QFutureCallOutEvent::Finished));
outputConnections.append(interface);
locker.unlock();
for (auto &&event : events)
interface->postCallOutEvent(*event);
}
void QFutureInterfaceBasePrivate::disconnectOutputInterface(QFutureCallOutInterface *interface)

View File

@ -38,6 +38,8 @@ class CanceledHandler;
template<class Function, class ResultType>
class FailureHandler;
#endif
class QBasicFutureWatcher;
}
class Q_CORE_EXPORT QFutureInterfaceBase
@ -176,6 +178,8 @@ private:
friend class QtPrivate::FailureHandler;
#endif
friend class QtPrivate::QBasicFutureWatcher;
template<class T>
friend class QPromise;

View File

@ -11,6 +11,7 @@
#include <QVarLengthArray>
#include <QSet>
#include <QList>
#include <private/qobject_p.h>
#include <QTest>
#include <qfuture.h>
@ -139,6 +140,18 @@ private:
std::function<void ()> m_fn;
};
// Emulates QWidget behavior by deleting its children early in the destructor
// instead of leaving it to ~QObject()
class FakeQWidget : public QObject
{
Q_OBJECT
public:
~FakeQWidget() override {
auto *d = QObjectPrivate::get(this);
d->deleteChildren();
}
};
using UniquePtr = std::unique_ptr<int>;
class tst_QFuture: public QObject
@ -3264,6 +3277,40 @@ void tst_QFuture::continuationsWithContext()
QCOMPARE(future.result(), 2);
}
// Cancellation when the context object is destroyed
{
// Use something like QWidget which deletes its children early, i.e.
// before ~QObject() runs. This behavior can lead to side-effects
// like QPointers to the parent not being set to nullptr during child
// object destruction.
QPointer shortLivedContext = new FakeQWidget();
shortLivedContext->moveToThread(&thread);
QPromise<int> promise;
auto future = promise.future()
.then(shortLivedContext, [&](int val) {
if (QThread::currentThread() != &thread)
return 0;
return val + 1000;
})
.onCanceled([&, ptr=QPointer(shortLivedContext)] {
if (QThread::currentThread() != &thread)
return 0;
if (ptr)
return 1;
return 2;
});
promise.start();
QMetaObject::invokeMethod(shortLivedContext, [&]() {
delete shortLivedContext;
}, Qt::BlockingQueuedConnection);
promise.finish();
QCOMPARE(future.result(), 2);
}
#ifndef QT_NO_EXCEPTIONS
// .onFaled()
{