Index: source/graphics/MapReader.cpp =================================================================== --- source/graphics/MapReader.cpp +++ source/graphics/MapReader.cpp @@ -1330,11 +1330,6 @@ { std::atomic progress{1}; Future task; - - ~GeneratorState() - { - task.CancelOrWait(); - } }; int CMapReader::StartMapGeneration(const CStrW& scriptFile) Index: source/graphics/TextureConverter.cpp =================================================================== --- source/graphics/TextureConverter.cpp +++ source/graphics/TextureConverter.cpp @@ -311,16 +311,7 @@ #endif // CONFIG2_NVTT } -CTextureConverter::~CTextureConverter() -{ -#if CONFIG2_NVTT - while (!m_ResultQueue.empty()) - { - m_ResultQueue.front().CancelOrWait(); - m_ResultQueue.pop(); - } -#endif // CONFIG2_NVTT -} +CTextureConverter::~CTextureConverter() = default; bool CTextureConverter::ConvertTexture(const CTexturePtr& texture, const VfsPath& src, const VfsPath& dest, const Settings& settings) { Index: source/ps/Future.h =================================================================== --- source/ps/Future.h +++ source/ps/Future.h @@ -57,12 +57,7 @@ {} ~Receiver() { - // For safety, wait on started task completion, but not on pending ones (auto-cancelled). - if (!Cancel()) - { - Wait(); - Cancel(); - } + ENSURE(IsDoneOrCanceled()); } Receiver(const Receiver&) = delete; @@ -145,7 +140,7 @@ * * Future is _not_ thread-safe. Call it from a single thread or ensure synchronization externally. * - * The destructor is never blocking. The promise may still be running on destruction. + * The callback never runs after the @p Future is destroied. * TODO: * - Handle exceptions. */ @@ -162,11 +157,19 @@ Future() = default; Future(const Future& o) = delete; Future(Future&&) = default; - Future& operator=(Future&&) = default; - ~Future() = default; + Future& operator=(Future&& other) + { + CancelOrWait(); + m_Receiver = std::move(other.m_Receiver); + return *this; + } + ~Future() + { + CancelOrWait(); + } /** - * Make the future wait for the result of @a func. + * Make the future wait for the result of @a callback. */ template PackagedTask Wrap(Callback&& callback); Index: source/ps/tests/test_Future.h =================================================================== --- source/ps/tests/test_Future.h +++ source/ps/tests/test_Future.h @@ -19,7 +19,9 @@ #include "ps/Future.h" +#include #include +#include #include class TestFuture : public CxxTest::TestSuite @@ -28,21 +30,72 @@ void test_future_basic() { int counter = 0; + Future noret; + auto task = noret.Wrap([&counter]{ counter++; }); + task(); + TS_ASSERT_EQUALS(counter, 1); + } + + template + static void CancelationTestHelper(Construct construct, Wait wait) + { + using namespace std::literals; + std::atomic taskStarted{false}; + auto [future, task] = construct([&] + { + taskStarted.store(true); + std::this_thread::sleep_for(100ms); + }); + const auto startTime = std::chrono::steady_clock::now(); + std::thread thread{task}; + // Wait some time to make sure the thread started the task. + while (!taskStarted.load()); + std::invoke(wait, future); + const auto stopTime = std::chrono::steady_clock::now(); + + // The task took longer then 100ms... + TS_ASSERT_LESS_THAN(100ms, stopTime - startTime); + // ...but not by much. + TS_ASSERT_LESS_THAN(stopTime - startTime, 106ms); + thread.join(); + } + + void test_cancelation() + { { - Future noret; - std::function task = noret.Wrap([&counter]() mutable { counter++; }); + int counter{0}; + auto task = [&] + { + Future noret; + return noret.Wrap([&counter]{ counter++; }); + // Auto-cancels the task. + }(); + // Even though the task is invoked, the callback doesn't get called. task(); - TS_ASSERT_EQUALS(counter, 1); + TS_ASSERT_EQUALS(counter, 0); } - { - Future noret; + constexpr auto normalFuture = [](auto callback) { - std::function task = noret.Wrap([&counter]() mutable { counter++; }); - // Auto-cancels the task. - } - } - TS_ASSERT_EQUALS(counter, 1); + Future future; + std::function task = future.Wrap(std::move(callback)); + return std::tuple{std::move(future), std::move(task)}; + }; + constexpr auto insideOptional = [](auto callback) + { + auto future = std::make_optional>(); + auto task = future->Wrap(std::move(callback)); + return std::tuple{std::move(future), std::move(task)}; + }; + + // There are many ways how to wait on a future. + CancelationTestHelper(normalFuture, &Future::Wait); + CancelationTestHelper(normalFuture, &Future::CancelOrWait); + CancelationTestHelper(normalFuture, [](Future& future) + { + future = {}; + }); + CancelationTestHelper(insideOptional, &std::optional>::reset); } void test_future_return() Index: source/simulation2/components/CCmpPathfinder.cpp =================================================================== --- source/simulation2/components/CCmpPathfinder.cpp +++ source/simulation2/components/CCmpPathfinder.cpp @@ -105,9 +105,6 @@ { SetDebugOverlay(false); // cleans up memory - // Wait on all pathfinding tasks. - for (Future& future : m_Futures) - future.CancelOrWait(); m_Futures.clear(); SAFE_DELETE(m_AtlasOverlay);