diff --git a/src/corelib/thread/qfuture_impl.h b/src/corelib/thread/qfuture_impl.h index 511caafdf7b..d7132caa7bb 100644 --- a/src/corelib/thread/qfuture_impl.h +++ b/src/corelib/thread/qfuture_impl.h @@ -425,7 +425,7 @@ bool Continuation::execute() { Q_ASSERT(parentFuture.isFinished()); - if (parentFuture.isCanceled()) { + if (parentFuture.d.isChainCanceled()) { #ifndef QT_NO_EXCEPTIONS if (parentFuture.d.hasException()) { // If the continuation doesn't take a QFuture argument, propagate the exception @@ -498,7 +498,7 @@ void Continuation::create(F &&func, } }; - f->d.setContinuation(std::move(continuation)); + f->d.setContinuation(std::move(continuation), p.d); } template @@ -527,7 +527,7 @@ void Continuation::create(F &&func, } }; - f->d.setContinuation(std::move(continuation)); + f->d.setContinuation(std::move(continuation), p.d); } template @@ -550,7 +550,7 @@ void Continuation::create(F &&func, }); }; - f->d.setContinuation(std::move(continuation)); + f->d.setContinuation(std::move(continuation), p.d); } template diff --git a/src/corelib/thread/qfutureinterface.cpp b/src/corelib/thread/qfutureinterface.cpp index e640114bb0d..7512d6174ad 100644 --- a/src/corelib/thread/qfutureinterface.cpp +++ b/src/corelib/thread/qfutureinterface.cpp @@ -834,8 +834,18 @@ void QFutureInterfaceBasePrivate::setState(QFutureInterfaceBase::State newState) } void QFutureInterfaceBase::setContinuation(std::function func) +{ + setContinuation(func, nullptr); +} + +void QFutureInterfaceBase::setContinuation(std::function func, + QFutureInterfaceBasePrivate *continuationFutureData) { QMutexLocker lock(&d->continuationMutex); + + if (continuationFutureData) + continuationFutureData->parentData = d; + // If the state is ready, run continuation immediately, // otherwise save it for later. if (isFinished()) { @@ -855,6 +865,24 @@ void QFutureInterfaceBase::runContinuation() const } } +bool QFutureInterfaceBase::isChainCanceled() const +{ + if (isCanceled()) + return true; + + auto parent = d->parentData; + while (parent) { + // If the future is in Canceled state because it had an exception, we want to + // continue checking the chain of parents for cancellation, otherwise if the exception + // is handeled inside the chain, it won't be interrupted even though cancellation has + // been requested. + if ((parent->state.loadRelaxed() & Canceled) && !parent->hasException) + return true; + parent = parent->parentData; + } + return false; +} + void QFutureInterfaceBase::setLaunchAsync(bool value) { d->launchAsync = value; diff --git a/src/corelib/thread/qfutureinterface.h b/src/corelib/thread/qfutureinterface.h index e813031d594..3130c914203 100644 --- a/src/corelib/thread/qfutureinterface.h +++ b/src/corelib/thread/qfutureinterface.h @@ -182,6 +182,8 @@ public: template static QFutureInterfaceBase get(const QFuture &future); // implemented in qfuture.h + bool isChainCanceled() const; + protected: // ### Qt 7: remove const from refT/derefT bool refT() const noexcept; @@ -212,6 +214,8 @@ private: protected: void setContinuation(std::function func); + void setContinuation(std::function func, + QFutureInterfaceBasePrivate *continuationFutureData); void runContinuation() const; void setLaunchAsync(bool value); diff --git a/src/corelib/thread/qfutureinterface_p.h b/src/corelib/thread/qfutureinterface_p.h index c92f8e2230c..fd7f34826de 100644 --- a/src/corelib/thread/qfutureinterface_p.h +++ b/src/corelib/thread/qfutureinterface_p.h @@ -190,6 +190,7 @@ public: QThreadPool *m_pool = nullptr; // Wrapper for continuation std::function continuation; + QFutureInterfaceBasePrivate *parentData = nullptr; RefCount refCount = 1; QAtomicInt state; // reads and writes can happen unprotected, both must be atomic diff --git a/tests/auto/corelib/thread/qfuture/tst_qfuture.cpp b/tests/auto/corelib/thread/qfuture/tst_qfuture.cpp index a6be2e9727a..9c8bd232d5a 100644 --- a/tests/auto/corelib/thread/qfuture/tst_qfuture.cpp +++ b/tests/auto/corelib/thread/qfuture/tst_qfuture.cpp @@ -143,6 +143,7 @@ private slots: void onFailedForMoveOnlyTypes(); #endif void onCanceled(); + void cancelContinuations(); void continuationsWithContext(); #if 0 // TODO: enable when QFuture::takeResults() is enabled @@ -2882,6 +2883,131 @@ void tst_QFuture::onCanceled() #endif // QT_NO_EXCEPTIONS } +void tst_QFuture::cancelContinuations() +{ + // The chain is cancelled in the middle of execution of continuations + { + QPromise promise; + + int checkpoint = 0; + auto future = promise.future().then([&](int value) { + ++checkpoint; + return value + 1; + }).then([&](int value) { + ++checkpoint; + promise.future().cancel(); + return value + 1; + }).then([&](int value) { + ++checkpoint; + return value + 1; + }).onCanceled([] { + return -1; + }); + + promise.start(); + promise.addResult(42); + promise.finish(); + + QCOMPARE(future.result(), -1); + QCOMPARE(checkpoint, 2); + } + + // The chain is cancelled before the execution of continuations + { + auto f = QtFuture::makeReadyFuture(42); + f.cancel(); + + int checkpoint = 0; + auto future = f.then([&](int value) { + ++checkpoint; + return value + 1; + }).then([&](int value) { + ++checkpoint; + return value + 1; + }).then([&](int value) { + ++checkpoint; + return value + 1; + }).onCanceled([] { + return -1; + }); + + QCOMPARE(future.result(), -1); + QCOMPARE(checkpoint, 0); + } + + // The chain is canceled partially, through an intermediate future + { + QPromise promise; + + int checkpoint = 0; + auto intermediate = promise.future().then([&](int value) { + ++checkpoint; + return value + 1; + }); + + auto future = intermediate.then([&](int value) { + ++checkpoint; + return value + 1; + }).then([&](int value) { + ++checkpoint; + return value + 1; + }).onCanceled([] { + return -1; + }); + + promise.start(); + promise.addResult(42); + + // This should cancel only the chain starting from intermediate + intermediate.cancel(); + + promise.finish(); + + QCOMPARE(future.result(), -1); + QCOMPARE(checkpoint, 1); + } + +#ifndef QT_NO_EXCEPTIONS + // The chain is cancelled in the middle of execution of continuations, + // while there's an exception in the chain, which is handeled inside + // the continuations. + { + QPromise promise; + + int checkpoint = 0; + auto future = promise.future().then([&](int value) { + ++checkpoint; + throw QException(); + return value + 1; + }).then([&](QFuture future) { + try { + auto res = future.result(); + Q_UNUSED(res); + } catch (const QException &) { + ++checkpoint; + } + return 2; + }).then([&](int value) { + ++checkpoint; + promise.future().cancel(); + return value + 1; + }).then([&](int value) { + ++checkpoint; + return value + 1; + }).onCanceled([] { + return -1; + }); + + promise.start(); + promise.addResult(42); + promise.finish(); + + QCOMPARE(future.result(), -1); + QCOMPARE(checkpoint, 3); + } +#endif // QT_NO_EXCEPTIONS +} + void tst_QFuture::continuationsWithContext() { QThread thread;