diff --git a/src/corelib/thread/qfuture_impl.h b/src/corelib/thread/qfuture_impl.h index 82c89cfa385..883f8d4f092 100644 --- a/src/corelib/thread/qfuture_impl.h +++ b/src/corelib/thread/qfuture_impl.h @@ -66,6 +66,10 @@ enum class Launch { Sync, Async, Inherit }; namespace QtPrivate { +template +using EnableIfSameOrConvertible = std::enable_if_t + || std::is_convertible_v>; + template using EnableForVoid = std::enable_if_t>; diff --git a/src/corelib/thread/qfutureinterface.cpp b/src/corelib/thread/qfutureinterface.cpp index 05482c40c21..1785815cf37 100644 --- a/src/corelib/thread/qfutureinterface.cpp +++ b/src/corelib/thread/qfutureinterface.cpp @@ -259,6 +259,37 @@ void QFutureInterfaceBase::waitForResume() d->pausedWaitCondition.wait(&d->m_mutex); } +void QFutureInterfaceBase::suspendIfRequested() +{ + const auto canSuspend = [] (int state) { + // can suspend only if 1) in any suspend-related state; 2) not canceled + return (state & suspendingOrSuspended) && !(state & Canceled); + }; + + // return early if possible to avoid taking the mutex lock. + { + const int state = d->state.loadRelaxed(); + if (!canSuspend(state)) + return; + } + + QMutexLocker lock(&d->m_mutex); + const int state = d->state.loadRelaxed(); + if (!canSuspend(state)) + return; + + // Note: expecting that Suspending and Suspended are mutually exclusive + if (!(state & Suspended)) { + // switch state in case this is the first invocation + switch_from_to(d->state, Suspending, Suspended); + d->sendCallOut(QFutureCallOutEvent(QFutureCallOutEvent::Suspended)); + } + + // decrease active thread count since this thread will wait. + const ThreadPoolThreadReleaser releaser(d->pool()); + d->pausedWaitCondition.wait(&d->m_mutex); +} + int QFutureInterfaceBase::progressValue() const { const QMutexLocker lock(&d->m_mutex); @@ -361,6 +392,11 @@ bool QFutureInterfaceBase::queryState(State state) const return d->state.loadRelaxed() & state; } +int QFutureInterfaceBase::loadState() const +{ + return d->state.loadRelaxed(); +} + void QFutureInterfaceBase::waitForResult(int resultIndex) { d->m_exceptionStore.throwPossibleException(); diff --git a/src/corelib/thread/qfutureinterface.h b/src/corelib/thread/qfutureinterface.h index c802be762b2..75c4cae0ca0 100644 --- a/src/corelib/thread/qfutureinterface.h +++ b/src/corelib/thread/qfutureinterface.h @@ -140,6 +140,7 @@ public: bool isThrottled() const; bool isResultReadyAt(int index) const; bool isValid() const; + int loadState() const; void cancel(); void setSuspended(bool suspend); @@ -151,6 +152,7 @@ public: bool waitForNextResult(); void waitForResult(int resultIndex); void waitForResume(); + void suspendIfRequested(); QMutex &mutex() const; QtPrivate::ExceptionStore &exceptionStore(); @@ -233,6 +235,7 @@ public: inline void reportResult(const T *result, int index = -1); inline void reportAndMoveResult(T &&result, int index = -1); + inline void reportResult(T &&result, int index = -1); inline void reportResult(const T &result, int index = -1); inline void reportResults(const QVector &results, int beginIndex = -1, int count = -1); inline void reportFinished(const T *result); @@ -285,6 +288,12 @@ void QFutureInterface::reportAndMoveResult(T &&result, int index) reportResultsReady(insertIndex, store.count()); } +template +void QFutureInterface::reportResult(T &&result, int index) +{ + reportAndMoveResult(std::move(result), index); +} + template inline void QFutureInterface::reportResult(const T &result, int index) { diff --git a/src/corelib/thread/qpromise.h b/src/corelib/thread/qpromise.h new file mode 100644 index 00000000000..15aea16f29b --- /dev/null +++ b/src/corelib/thread/qpromise.h @@ -0,0 +1,128 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QPROMISE_H +#define QPROMISE_H + +#include +#include + +QT_REQUIRE_CONFIG(future); + +QT_BEGIN_NAMESPACE + +template +class QPromise +{ + static_assert (std::is_copy_constructible_v + || std::is_move_constructible_v + || std::is_same_v, + "Type with copy or move constructors or type void is required"); +public: + QPromise() = default; + Q_DISABLE_COPY(QPromise) + QPromise(QPromise &&other) : d(other.d) + { + // In constructor, there's no need to perform swap(). Assign new + // QFutureInterface to other.d instead which is slightly cheaper. + other.d = QFutureInterface(); + } + QPromise& operator=(QPromise &&other) + { + swap(other); + return *this; + } + ~QPromise() + { + const int state = d.loadState(); + // If QFutureInterface has no state, there is nothing to be done + if (state == static_cast(QFutureInterfaceBase::State::NoState)) + return; + // Otherwise, if computation is not finished at this point, cancel + // potential waits + if (!(state & QFutureInterfaceBase::State::Finished)) { + d.cancel(); + reportFinished(); // required to finalize the state + } + } + + // Core QPromise APIs + QFuture future() const { return d.future(); } + template>, + typename = QtPrivate::EnableIfSameOrConvertible, std::decay_t>> + void addResult(U &&result, int index = -1) + { + d.reportResult(std::forward(result), index); + } +#ifndef QT_NO_EXCEPTIONS + void setException(const QException &e) { d.reportException(e); } + void setException(std::exception_ptr e) { d.reportException(e); } +#endif + void reportStarted() { d.reportStarted(); } + void reportFinished() { d.reportFinished(); } + + void suspendIfRequested() { d.suspendIfRequested(); } + + bool isCanceled() const { return d.isCanceled(); } + + // Progress methods + void setProgressRange(int minimum, int maximum) { d.setProgressRange(minimum, maximum); } + void setProgressValue(int progressValue) { d.setProgressValue(progressValue); } + void setProgressValueAndText(int progressValue, const QString &progressText) + { + d.setProgressValueAndText(progressValue, progressText); + } + +private: + mutable QFutureInterface d = QFutureInterface(); + + void swap(QPromise &other) + { + // Note: copy operations are expensive! They trigger several atomic + // reference counts + auto tmp = this->d; + this->d = other.d; + other.d = tmp; + } +}; + +QT_END_NAMESPACE + +#endif // QPROMISE_H diff --git a/src/corelib/thread/thread.pri b/src/corelib/thread/thread.pri index e3d791fee7b..d36c8011ef4 100644 --- a/src/corelib/thread/thread.pri +++ b/src/corelib/thread/thread.pri @@ -72,7 +72,8 @@ qtConfig(future) { thread/qfuturesynchronizer.h \ thread/qfuturewatcher.h \ thread/qfuturewatcher_p.h \ - thread/qresultstore.h + thread/qresultstore.h \ + thread/qpromise.h SOURCES += \ thread/qexception.cpp \ diff --git a/tests/auto/corelib/thread/CMakeLists.txt b/tests/auto/corelib/thread/CMakeLists.txt index d07c583a77e..b73d9af0594 100644 --- a/tests/auto/corelib/thread/CMakeLists.txt +++ b/tests/auto/corelib/thread/CMakeLists.txt @@ -18,6 +18,7 @@ if(QT_FEATURE_thread) add_subdirectory(qthreadstorage) add_subdirectory(qwaitcondition) add_subdirectory(qwritelocker) + add_subdirectory(qpromise) endif() if(TARGET Qt::Concurrent) add_subdirectory(qfuturewatcher) diff --git a/tests/auto/corelib/thread/qpromise/CMakeLists.txt b/tests/auto/corelib/thread/qpromise/CMakeLists.txt new file mode 100644 index 00000000000..3cc20cafd7f --- /dev/null +++ b/tests/auto/corelib/thread/qpromise/CMakeLists.txt @@ -0,0 +1,12 @@ +##################################################################### +## tst_qpromise Test: +##################################################################### + +qt_add_test(tst_qpromise + SOURCES + tst_qpromise.cpp + PUBLIC_LIBRARIES + Qt::CorePrivate + LIBRARIES + Threads::Threads # solves issue with libpthread linkage +) diff --git a/tests/auto/corelib/thread/qpromise/qpromise.pro b/tests/auto/corelib/thread/qpromise/qpromise.pro new file mode 100644 index 00000000000..9ea1aaacdfe --- /dev/null +++ b/tests/auto/corelib/thread/qpromise/qpromise.pro @@ -0,0 +1,4 @@ +CONFIG += testcase +TARGET = tst_qpromise +QT = core core-private testlib +SOURCES = tst_qpromise.cpp diff --git a/tests/auto/corelib/thread/qpromise/tst_qpromise.cpp b/tests/auto/corelib/thread/qpromise/tst_qpromise.cpp new file mode 100644 index 00000000000..1e5864e59ee --- /dev/null +++ b/tests/auto/corelib/thread/qpromise/tst_qpromise.cpp @@ -0,0 +1,577 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +#include +#include + +#define QPROMISE_TEST + +#include +#include +#include +#include + +#include +#include +#include + +class tst_QPromise : public QObject +{ + Q_OBJECT +private slots: + // simple test cases + void promise(); + void futureFromPromise(); + void addResult(); + void addResultOutOfOrder(); +#ifndef QT_NO_EXCEPTIONS + void setException(); +#endif + void cancel(); + void progress(); + + // complicated test cases + void addInThread(); + void addInThreadMoveOnlyObject(); // separate test case - QTBUG-84736 + void reportFromMultipleThreads(); + void reportFromMultipleThreadsByMovedPromise(); + void doNotCancelWhenFinished(); +#ifndef QT_NO_EXCEPTIONS + void cancelWhenDestroyed(); +#endif + void cancelWhenReassigned(); + void finishWhenMoved(); + void cancelWhenMoved(); + void waitUntilResumed(); + void waitUntilCanceled(); +}; + +struct TrivialType { int field = 0; }; +struct CopyOnlyType { + Q_DISABLE_MOVE(CopyOnlyType); + CopyOnlyType() = default; + CopyOnlyType(const CopyOnlyType &) = default; + CopyOnlyType& operator=(const CopyOnlyType &) = default; + ~CopyOnlyType() = default; + + int field = 0; +}; +struct MoveOnlyType { + Q_DISABLE_COPY(MoveOnlyType); + MoveOnlyType() = default; + MoveOnlyType(MoveOnlyType &&) = default; + MoveOnlyType& operator=(MoveOnlyType &&) = default; + ~MoveOnlyType() = default; + + int field = 0; +}; +bool operator==(const CopyOnlyType &a, const CopyOnlyType &b) { return a.field == b.field; } +bool operator==(const MoveOnlyType &a, const MoveOnlyType &b) { return a.field == b.field; } + +// A wrapper for a test function, calls the function, if it fails, reports failure +#define RUN_TEST_FUNC(test, ...) \ +do { \ + test(__VA_ARGS__); \ + if (QTest::currentTestFailed()) \ + QFAIL("Test case " #test "(" #__VA_ARGS__ ") failed"); \ +} while (false) + +// std::thread-like wrapper that ensures that the thread is joined at the end of +// a scope to prevent potential std::terminate +struct ThreadWrapper +{ + std::unique_ptr t; + template + ThreadWrapper(Function &&f) : t(QThread::create(std::forward(f))) + { + t->start(); + } + void join() { t->wait(); } + ~ThreadWrapper() + { + t->wait(); + } +}; + +void tst_QPromise::promise() +{ + const auto testCanCreatePromise = [] (auto promise) { + promise.reportStarted(); + promise.suspendIfRequested(); // should not block on its own + promise.reportFinished(); + }; + + RUN_TEST_FUNC(testCanCreatePromise, QPromise()); + RUN_TEST_FUNC(testCanCreatePromise, QPromise()); + RUN_TEST_FUNC(testCanCreatePromise, QPromise>()); + RUN_TEST_FUNC(testCanCreatePromise, QPromise()); + RUN_TEST_FUNC(testCanCreatePromise, QPromise()); + RUN_TEST_FUNC(testCanCreatePromise, QPromise()); +} + +void tst_QPromise::futureFromPromise() +{ + const auto testCanCreateFutureFromPromise = [] (auto promise) { + auto future = promise.future(); + QVERIFY(!future.isValid()); + + promise.reportStarted(); + QCOMPARE(future.isStarted(), true); + QVERIFY(future.isValid()); + + promise.reportFinished(); + QCOMPARE(future.isFinished(), true); + QVERIFY(future.isValid()); + + future.waitForFinished(); + }; + + RUN_TEST_FUNC(testCanCreateFutureFromPromise, QPromise()); + RUN_TEST_FUNC(testCanCreateFutureFromPromise, QPromise()); + RUN_TEST_FUNC(testCanCreateFutureFromPromise, QPromise>()); + RUN_TEST_FUNC(testCanCreateFutureFromPromise, QPromise()); + RUN_TEST_FUNC(testCanCreateFutureFromPromise, QPromise()); + RUN_TEST_FUNC(testCanCreateFutureFromPromise, QPromise()); +} + +void tst_QPromise::addResult() +{ + QPromise promise; + auto f = promise.future(); + + // add as lvalue + { + int result = 456; + promise.addResult(result); + QCOMPARE(f.resultCount(), 1); + QCOMPARE(f.result(), result); + QCOMPARE(f.resultAt(0), result); + } + // add as rvalue + { + int result = 789; + promise.addResult(789); + QCOMPARE(f.resultCount(), 2); + QCOMPARE(f.resultAt(1), result); + } + // add at position + { + int result = 56238; + promise.addResult(result, 2); + QCOMPARE(f.resultCount(), 3); + QCOMPARE(f.resultAt(2), result); + } + // add at position and overwrite + { + int result = -1; + const auto originalCount = f.resultCount(); + promise.addResult(result, 0); + QCOMPARE(f.resultCount(), originalCount); + QCOMPARE(f.resultAt(0), result); + } +} + +void tst_QPromise::addResultOutOfOrder() +{ + // Compare results available in QFuture to expected results + const auto compareResults = [] (const auto &future, auto expected) { + QCOMPARE(future.resultCount(), expected.size()); + // index based loop + for (int i = 0; i < future.resultCount(); ++i) + QCOMPARE(future.resultAt(i), expected.at(i)); + // iterator based loop + QVERIFY(std::equal(future.begin(), future.end(), expected.begin())); + }; + + // out of order results without a gap + { + QPromise promise; + auto f = promise.future(); + promise.addResult(456, 1); + QCOMPARE(f.resultCount(), 0); + promise.addResult(123, 0); + + QList expected({123, 456}); + RUN_TEST_FUNC(compareResults, f, expected); + QCOMPARE(f.results(), expected); + } + + // out of order results with a gap that is closed "later" + { + QPromise promise; + auto f = promise.future(); + promise.addResult(0, 0); + promise.addResult(1, 1); + promise.addResult(3, 3); // intentional gap here + + QList expectedWhenGapExists({0, 1}); + RUN_TEST_FUNC(compareResults, f, expectedWhenGapExists); + QCOMPARE(f.resultAt(3), 3); + + QList expectedWhenNoGap({0, 1, 2, 3}); + promise.addResult(2, 2); // fill a gap with a value + RUN_TEST_FUNC(compareResults, f, expectedWhenNoGap); + QCOMPARE(f.results(), expectedWhenNoGap); + } +} + +#ifndef QT_NO_EXCEPTIONS +void tst_QPromise::setException() +{ + struct TestException {}; // custom exception class + const auto testExceptionCaught = [] (auto promise, const auto& exception) { + auto f = promise.future(); + promise.reportStarted(); + promise.setException(exception); + promise.reportFinished(); + + bool caught = false; + try { + f.waitForFinished(); + } catch (const QException&) { + caught = true; + } catch (const TestException&) { + caught = true; + } + QVERIFY(caught); + }; + + RUN_TEST_FUNC(testExceptionCaught, QPromise(), QException()); + RUN_TEST_FUNC(testExceptionCaught, QPromise(), QException()); + RUN_TEST_FUNC(testExceptionCaught, QPromise(), + std::make_exception_ptr(TestException())); + RUN_TEST_FUNC(testExceptionCaught, QPromise(), + std::make_exception_ptr(TestException())); +} +#endif + +void tst_QPromise::cancel() +{ + const auto testCancel = [] (auto promise) { + auto f = promise.future(); + f.cancel(); + QCOMPARE(promise.isCanceled(), true); + }; + + testCancel(QPromise()); + testCancel(QPromise()); +} + +void tst_QPromise::progress() +{ + const auto testProgress = [] (auto promise) { + auto f = promise.future(); + + promise.setProgressRange(0, 2); + QCOMPARE(f.progressMinimum(), 0); + QCOMPARE(f.progressMaximum(), 2); + + QCOMPARE(f.progressValue(), 0); + promise.setProgressValue(1); + QCOMPARE(f.progressValue(), 1); + promise.setProgressValue(0); // decrement + QCOMPARE(f.progressValue(), 1); + promise.setProgressValue(10); // out of range + QEXPECT_FAIL("", "Out of range value is set - QTBUG-84729", Continue); + QCOMPARE(f.progressValue(), 1); + + promise.setProgressRange(0, 100); + promise.setProgressValueAndText(50, u8"50%"); + QCOMPARE(f.progressValue(), 50); + QCOMPARE(f.progressText(), u8"50%"); + }; + + RUN_TEST_FUNC(testProgress, QPromise()); + RUN_TEST_FUNC(testProgress, QPromise()); +} + +void tst_QPromise::addInThread() +{ + const auto testAddResult = [] (auto promise, const auto &result) { + promise.reportStarted(); + auto f = promise.future(); + // move construct QPromise + ThreadWrapper thr([p = std::move(promise), &result] () mutable { + p.addResult(result); + }); + // Waits for result first + QCOMPARE(f.result(), result); + QCOMPARE(f.resultAt(0), result); + }; + + RUN_TEST_FUNC(testAddResult, QPromise(), 42); + RUN_TEST_FUNC(testAddResult, QPromise(), u8"42"); + RUN_TEST_FUNC(testAddResult, QPromise(), CopyOnlyType{99}); +} + +void tst_QPromise::addInThreadMoveOnlyObject() +{ + QPromise promise; + promise.reportStarted(); + auto f = promise.future(); + + ThreadWrapper thr([p = std::move(promise)] () mutable { + p.addResult(MoveOnlyType{-11}); + }); + + // Iterators wait for result first + for (auto& result : f) + QCOMPARE(result, MoveOnlyType{-11}); +} + +void tst_QPromise::reportFromMultipleThreads() +{ + QPromise promise; + auto f = promise.future(); + promise.reportStarted(); + + ThreadWrapper threads[] = { + ThreadWrapper([&promise] () mutable { promise.addResult(42); }), + ThreadWrapper([&promise] () mutable { promise.addResult(43); }), + ThreadWrapper([&promise] () mutable { promise.addResult(44); }), + }; + for (auto& t : threads) + t.join(); + promise.reportFinished(); + + QList expected = {42, 43, 44}; + for (auto actual : f.results()) { + QVERIFY(std::find(expected.begin(), expected.end(), actual) != expected.end()); + expected.removeOne(actual); + } +} + +void tst_QPromise::reportFromMultipleThreadsByMovedPromise() +{ + QPromise initialPromise; + auto f = initialPromise.future(); + { + // Move QPromise into local scope: local QPromise (as being + // move-constructed) must be able to set results, QFuture must still + // hold correct references to results. + auto promise = std::move(initialPromise); + promise.reportStarted(); + ThreadWrapper threads[] = { + ThreadWrapper([&promise] () mutable { promise.addResult(42); }), + ThreadWrapper([&promise] () mutable { promise.addResult(43); }), + ThreadWrapper([&promise] () mutable { promise.addResult(44); }), + }; + for (auto& t : threads) + t.join(); + promise.reportFinished(); + } + + QCOMPARE(f.isFinished(), true); + QCOMPARE(f.isValid(), true); + + QList expected = {42, 43, 44}; + for (auto actual : f.results()) { + QVERIFY(std::find(expected.begin(), expected.end(), actual) != expected.end()); + expected.removeOne(actual); + } +} + +void tst_QPromise::doNotCancelWhenFinished() +{ + const auto testFinishedPromise = [] (auto promise) { + auto f = promise.future(); + promise.reportStarted(); + + // Finish QPromise inside thread, destructor must not call cancel() + ThreadWrapper([p = std::move(promise)] () mutable { p.reportFinished(); }).join(); + + f.waitForFinished(); + + QCOMPARE(f.isFinished(), true); + QCOMPARE(f.isCanceled(), false); + }; + + RUN_TEST_FUNC(testFinishedPromise, QPromise()); + RUN_TEST_FUNC(testFinishedPromise, QPromise()); + RUN_TEST_FUNC(testFinishedPromise, QPromise()); +} + +#ifndef QT_NO_EXCEPTIONS +void tst_QPromise::cancelWhenDestroyed() +{ + QPromise initialPromise; + auto f = initialPromise.future(); + + try { + // Move QPromise to local scope. On destruction, it must call cancel(). + auto promise = std::move(initialPromise); + promise.reportStarted(); + ThreadWrapper threads[] = { + ThreadWrapper([&promise] () mutable { promise.addResult(42); }), + ThreadWrapper([&promise] () mutable { promise.addResult(43); }), + ThreadWrapper([&promise] () mutable { promise.addResult(44); }), + }; + for (auto& t : threads) + t.join(); + throw "Throw in the middle, we lose our promise here, reportFinished() not called!"; + promise.reportFinished(); + } catch (...) {} + + QCOMPARE(f.isFinished(), true); + QCOMPARE(f.isCanceled(), true); + + // Results are still available despite throw + QList expected = {42, 43, 44}; + for (auto actual : f.results()) { + QVERIFY(std::find(expected.begin(), expected.end(), actual) != expected.end()); + expected.removeOne(actual); + } +} +#endif + +void tst_QPromise::cancelWhenReassigned() +{ + QPromise promise; + auto f = promise.future(); + promise.reportStarted(); + + ThreadWrapper thr([p = std::move(promise)] () mutable { + QThread::msleep(100); + p = QPromise(); // assign new promise, old must be correctly destroyed + }); + + f.waitForFinished(); // wait for the old promise + + QCOMPARE(f.isFinished(), true); + QCOMPARE(f.isCanceled(), true); +} + +void tst_QPromise::finishWhenMoved() +{ + QPromise promise1; + auto f1 = promise1.future(); + promise1.reportStarted(); + + QPromise promise2; + auto f2 = promise2.future(); + promise2.reportStarted(); + + ThreadWrapper thr([&promise1, &promise2] () mutable { + QThread::msleep(100); + // There is swap semantics in move, so promise #1 and #2 just swap + promise1 = std::move(promise2); + promise1.reportFinished(); // this finish is for future #2 + promise2.reportFinished(); // this finish is for future #1 + }); + + f1.waitForFinished(); + f2.waitForFinished(); + + // Future #1 and #2 are finished inside thread + QCOMPARE(f1.isFinished(), true); + QCOMPARE(f1.isCanceled(), false); + + QCOMPARE(f2.isFinished(), true); + QCOMPARE(f2.isCanceled(), false); +} + +void tst_QPromise::cancelWhenMoved() +{ + QPromise promise1; + auto f1 = promise1.future(); + promise1.reportStarted(); + + QPromise promise2; + auto f2 = promise2.future(); + promise2.reportStarted(); + + // Move promises to local scope to test cancellation behavior + ThreadWrapper thr([p1 = std::move(promise1), p2 = std::move(promise2)] () mutable { + QThread::msleep(100); + // There is swap semantics in move, so promise #1 and #2 just swap + p1 = std::move(p2); + p1.reportFinished(); // this finish is for future #2 + }); + + f1.waitForFinished(); + f2.waitForFinished(); + + // Future #1 is implicitly cancelled inside thread + QCOMPARE(f1.isFinished(), true); + QCOMPARE(f1.isCanceled(), true); + + // Future #2 is explicitly finished inside thread + QCOMPARE(f2.isFinished(), true); + QCOMPARE(f2.isCanceled(), false); +} + +void tst_QPromise::waitUntilResumed() +{ + QPromise promise; + promise.reportStarted(); + auto f = promise.future(); + f.suspend(); + + ThreadWrapper thr([p = std::move(promise)] () mutable { + p.suspendIfRequested(); + p.addResult(42); // result added after suspend + p.reportFinished(); + }); + + while (!f.isSuspended()) { // busy wait until worker thread suspends + QCOMPARE(f.isFinished(), false); // exit condition in case of failure + QThread::msleep(50); // allow another thread to actually carry on + } + + f.resume(); + f.waitForFinished(); + + QCOMPARE(f.resultCount(), 1); + QCOMPARE(f.result(), 42); +} + +void tst_QPromise::waitUntilCanceled() +{ + QPromise promise; + promise.reportStarted(); + auto f = promise.future(); + f.suspend(); + + ThreadWrapper thr([p = std::move(promise)] () mutable { + p.suspendIfRequested(); + p.addResult(42); // result not added due to QFuture::cancel() + p.reportFinished(); + }); + + while (!f.isSuspended()) { // busy wait until worker thread suspends + QCOMPARE(f.isFinished(), false); // exit condition in case of failure + QThread::msleep(50); // allow another thread to actually carry on + } + + f.cancel(); + f.waitForFinished(); + + QCOMPARE(f.resultCount(), 0); +} + +QTEST_MAIN(tst_QPromise) +#include "tst_qpromise.moc" diff --git a/tests/auto/corelib/thread/thread.pro b/tests/auto/corelib/thread/thread.pro index 90b8d6806e3..510614ef66f 100644 --- a/tests/auto/corelib/thread/thread.pro +++ b/tests/auto/corelib/thread/thread.pro @@ -18,7 +18,8 @@ qtConfig(thread) { qthreadpool \ qthreadstorage \ qwaitcondition \ - qwritelocker + qwritelocker \ + qpromise } qtHaveModule(concurrent) {