diff options
-rw-r--r-- | src/corelib/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/corelib/doc/snippets/code/src_corelib_thread_qfuture.cpp | 13 | ||||
-rw-r--r-- | src/corelib/thread/qbasicfuturewatcher.cpp | 80 | ||||
-rw-r--r-- | src/corelib/thread/qbasicfuturewatcher.h | 39 | ||||
-rw-r--r-- | src/corelib/thread/qfuture.qdoc | 22 | ||||
-rw-r--r-- | src/corelib/thread/qfuture_impl.h | 85 | ||||
-rw-r--r-- | src/corelib/thread/qfutureinterface.cpp | 47 | ||||
-rw-r--r-- | src/corelib/thread/qfutureinterface.h | 4 | ||||
-rw-r--r-- | tests/auto/corelib/thread/qfuture/tst_qfuture.cpp | 47 |
9 files changed, 275 insertions, 63 deletions
diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt index 42364f6f4a..6b9fbfe77c 100644 --- a/src/corelib/CMakeLists.txt +++ b/src/corelib/CMakeLists.txt @@ -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 diff --git a/src/corelib/doc/snippets/code/src_corelib_thread_qfuture.cpp b/src/corelib/doc/snippets/code/src_corelib_thread_qfuture.cpp index bbd61d6dd3..500d7cc7a5 100644 --- a/src/corelib/doc/snippets/code/src_corelib_thread_qfuture.cpp +++ b/src/corelib/doc/snippets/code/src_corelib_thread_qfuture.cpp @@ -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] diff --git a/src/corelib/thread/qbasicfuturewatcher.cpp b/src/corelib/thread/qbasicfuturewatcher.cpp new file mode 100644 index 0000000000..2602995e8f --- /dev/null +++ b/src/corelib/thread/qbasicfuturewatcher.cpp @@ -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" diff --git a/src/corelib/thread/qbasicfuturewatcher.h b/src/corelib/thread/qbasicfuturewatcher.h new file mode 100644 index 0000000000..49db7284e7 --- /dev/null +++ b/src/corelib/thread/qbasicfuturewatcher.h @@ -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 diff --git a/src/corelib/thread/qfuture.qdoc b/src/corelib/thread/qfuture.qdoc index 7d0f639417..79bd121aee 100644 --- a/src/corelib/thread/qfuture.qdoc +++ b/src/corelib/thread/qfuture.qdoc @@ -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. diff --git a/src/corelib/thread/qfuture_impl.h b/src/corelib/thread/qfuture_impl.h index 47a07801c7..53f2326cde 100644 --- a/src/corelib/thread/qfuture_impl.h +++ b/src/corelib/thread/qfuture_impl.h @@ -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); - - 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(); - }); + Q_ASSERT(context); + + // 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> diff --git a/src/corelib/thread/qfutureinterface.cpp b/src/corelib/thread/qfutureinterface.cpp index 7784ec0939..5556551447 100644 --- a/src/corelib/thread/qfutureinterface.cpp +++ b/src/corelib/thread/qfutureinterface.cpp @@ -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) diff --git a/src/corelib/thread/qfutureinterface.h b/src/corelib/thread/qfutureinterface.h index ecb0f5b7ea..4db3ebb859 100644 --- a/src/corelib/thread/qfutureinterface.h +++ b/src/corelib/thread/qfutureinterface.h @@ -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; diff --git a/tests/auto/corelib/thread/qfuture/tst_qfuture.cpp b/tests/auto/corelib/thread/qfuture/tst_qfuture.cpp index 08c417019e..79ca38209e 100644 --- a/tests/auto/corelib/thread/qfuture/tst_qfuture.cpp +++ b/tests/auto/corelib/thread/qfuture/tst_qfuture.cpp @@ -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() { |