From fff137826866df8150c71a38edc3b74b60037d47 Mon Sep 17 00:00:00 2001 From: Eric Niebler Date: Sun, 15 Mar 2026 15:15:55 -0700 Subject: [PATCH 01/11] guard against infinite recursion from too many inline completions in `task` --- include/stdexec/__detail/__as_awaitable.hpp | 217 +++++++++++++------- 1 file changed, 145 insertions(+), 72 deletions(-) diff --git a/include/stdexec/__detail/__as_awaitable.hpp b/include/stdexec/__detail/__as_awaitable.hpp index 2270d39c4..861d42ba1 100644 --- a/include/stdexec/__detail/__as_awaitable.hpp +++ b/include/stdexec/__detail/__as_awaitable.hpp @@ -17,6 +17,7 @@ #include "__execution_fwd.hpp" +#include "__atomic.hpp" #include "__awaitable.hpp" #include "__completion_signatures_of.hpp" #include "__concepts.hpp" @@ -31,6 +32,9 @@ #include #include +STDEXEC_PRAGMA_PUSH() +STDEXEC_PRAGMA_IGNORE_GNU("-Wmissing-braces") + namespace STDEXEC { #if !STDEXEC_NO_STDCPP_COROUTINES() @@ -46,11 +50,11 @@ namespace STDEXEC inline constexpr __mconst __as_single<0>; template - using __single_value = __minvoke), _Values...>; + using __single_value_t = __minvoke), _Values...>; template using __value_t = __decay_t< - __value_types_of_t<_Sender, env_of_t<_Promise&>, __q<__single_value>, __msingle_or>>; + __value_types_of_t<_Sender, env_of_t<_Promise&>, __q<__single_value_t>, __msingle_or>>; inline constexpr auto __get_await_completion_adaptor = __with_default{get_await_completion_adaptor, std::identity{}}; @@ -93,6 +97,45 @@ namespace STDEXEC && __completes_inline_for && __completes_inline_for; + template + struct __sender_awaitable_base; + + template + struct __sender_awaitable_base<_Value, true> + { + static constexpr auto await_ready() noexcept -> bool + { + return false; + } + + constexpr auto await_resume() -> _Value + { + // If the operation completed with set_stopped (as denoted by the monostate + // alternative being active), we should not be resuming this coroutine at all. + STDEXEC_ASSERT(__result_.index() != 0); + if (__result_.index() == 2) + { + // The operation completed with set_error, so we need to rethrow the exception. + std::rethrow_exception(std::move(std::get<2>(__result_))); + } + // The operation completed with set_value, so we can just return the value, which + // may be void. + using __reference_t = std::add_rvalue_reference_t<_Value>; + return static_cast<__reference_t>(std::get<1>(__result_)); + } + + __std::coroutine_handle<> __continuation_; + __expected_t<_Value> __result_{}; + }; + + // When the sender is not statically known to complete inline, we need to use atomic + // state to guard against too many inline completions causing a stack overflow. + template + struct __sender_awaitable_base<_Value, false> : __sender_awaitable_base<_Value, true> + { + __std::atomic __ready_{false}; + }; + template struct __receiver_base { @@ -103,11 +146,11 @@ namespace STDEXEC { STDEXEC_TRY { - __result_.template emplace<1>(static_cast<_Us&&>(__us)...); + __awaiter_.__result_.template emplace<1>(static_cast<_Us&&>(__us)...); } STDEXEC_CATCH_ALL { - __result_.template emplace<2>(std::current_exception()); + __awaiter_.__result_.template emplace<2>(std::current_exception()); } } @@ -115,23 +158,25 @@ namespace STDEXEC void set_error(_Error&& __err) noexcept { if constexpr (__decays_to<_Error, std::exception_ptr>) - __result_.template emplace<2>(static_cast<_Error&&>(__err)); + __awaiter_.__result_.template emplace<2>(static_cast<_Error&&>(__err)); else if constexpr (__decays_to<_Error, std::error_code>) - __result_.template emplace<2>(std::make_exception_ptr(std::system_error(__err))); + __awaiter_.__result_.template emplace<2>( + std::make_exception_ptr(std::system_error(__err))); else - __result_.template emplace<2>(std::make_exception_ptr(static_cast<_Error&&>(__err))); + __awaiter_.__result_.template emplace<2>( + std::make_exception_ptr(static_cast<_Error&&>(__err))); } - __expected_t<_Value>& __result_; + __sender_awaitable_base<_Value, true>& __awaiter_; }; template struct __sync_receiver : __receiver_base<_Value> { - constexpr explicit __sync_receiver(__expected_t<_Value>& __result, - __std::coroutine_handle<_Promise> __continuation) noexcept - : __receiver_base<_Value>{__result} - , __continuation_{__continuation} + using __awaiter_t = __sender_awaitable_base<_Value, true>; + + constexpr explicit __sync_receiver(__awaiter_t& __awaiter) noexcept + : __receiver_base<_Value>{__awaiter} {} void set_stopped() noexcept @@ -141,35 +186,37 @@ namespace STDEXEC } // Forward get_env query to the coroutine promise + [[nodiscard]] constexpr auto get_env() const noexcept -> env_of_t<_Promise&> { - return STDEXEC::get_env(__continuation_.promise()); + auto __pcoro = this->__awaiter_.__continuation_.address(); + auto __hcoro = __std::coroutine_handle<_Promise>::from_address(__pcoro); + return STDEXEC::get_env(__hcoro.promise()); } - - __std::coroutine_handle<_Promise> __continuation_; }; // The receiver type used to connect to senders that could complete asynchronously. template struct __async_receiver : __sync_receiver<_Promise, _Value> { - constexpr explicit __async_receiver(__expected_t<_Value>& __result, - __std::coroutine_handle<_Promise> __continuation) noexcept - : __sync_receiver<_Promise, _Value>{__result, __continuation} + using __awaiter_t = __sender_awaitable_base<_Value, false>; + + constexpr explicit __async_receiver(__awaiter_t& __awaiter) noexcept + : __sync_receiver<_Promise, _Value>{__awaiter} {} template void set_value(_Us&&... __us) noexcept { this->__sync_receiver<_Promise, _Value>::set_value(static_cast<_Us&&>(__us)...); - this->__continuation_.resume(); + __done(); } template void set_error(_Error&& __err) noexcept { this->__sync_receiver<_Promise, _Value>::set_error(static_cast<_Error&&>(__err)); - this->__continuation_.resume(); + __done(); } constexpr void set_stopped() noexcept @@ -179,14 +226,36 @@ namespace STDEXEC // Resuming the stopped continuation unwinds the coroutine stack until we reach // a promise that can handle the stopped signal. The coroutine referred to by // __continuation_ will never be resumed. - __std::coroutine_handle<> __on_stopped = - this->__continuation_.promise().unhandled_stopped(); + auto __pcoro = this->__awaiter_.__continuation_.address(); + auto __hcoro = __std::coroutine_handle<_Promise>::from_address(__pcoro); + auto& __promise = __hcoro.promise(); + __std::coroutine_handle<> __on_stopped = __promise.unhandled_stopped(); __on_stopped.resume(); } STDEXEC_CATCH_ALL { - this->__result_.template emplace<2>(std::current_exception()); - this->__continuation_.resume(); + this->__awaiter_.__result_.template emplace<2>(std::current_exception()); + this->__awaiter_.__continuation_.resume(); + } + } + + private: + void __done() noexcept + { + // If __ready_ is still false, then we are completing inline. Update the + // value of __ready_. Otherwise, we are completing asynchronously, so we invoke + // the continuation without unwinding the stack. + auto __expected = false; + auto& __awaiter = static_cast<__awaiter_t&>(this->__awaiter_); + if (!__awaiter.__ready_.compare_exchange_strong(__expected, + true, + __std::memory_order_release, + __std::memory_order_acquire)) + { + // We get here if __ready_ was true. It got set to true in await_suspend() + // immediately after the operation was started, which implies that the operation + // completed asynchronously, so we need to resume the continuation. + __awaiter.__continuation_.resume(); } } }; @@ -197,49 +266,49 @@ namespace STDEXEC template using __async_receiver_t = __async_receiver<_Promise, __detail::__value_t<_Sender, _Promise>>; - template - struct __sender_awaitable_base - { - static constexpr auto await_ready() noexcept -> bool - { - return false; - } - - constexpr auto await_resume() -> _Value - { - // If the operation completed with set_stopped (as denoted by the monostate - // alternative being active), we should not be resuming this coroutine at all. - STDEXEC_ASSERT(__result_.index() != 0); - if (__result_.index() == 2) - { - // The operation completed with set_error, so we need to rethrow the exception. - std::rethrow_exception(std::move(std::get<2>(__result_))); - } - // The operation completed with set_value, so we can just return the value, which - // may be void. - return static_cast>(std::get<1>(__result_)); - } - - protected: - __expected_t<_Value> __result_{}; - }; - ////////////////////////////////////////////////////////////////////////////////////// // __sender_awaitable: awaitable type returned by as_awaitable when given a sender // that does not have an as_awaitable member function template - struct __sender_awaitable : __sender_awaitable_base<__detail::__value_t<_Sender, _Promise>> + struct __sender_awaitable + : __sender_awaitable_base<__detail::__value_t<_Sender, _Promise>, false> { + using __value_t = __detail::__value_t<_Sender, _Promise>; + constexpr explicit __sender_awaitable(_Sender&& __sndr, __std::coroutine_handle<_Promise> __hcoro) noexcept(__nothrow_connectable<_Sender, __receiver_t>) - : __opstate_(STDEXEC::connect(static_cast<_Sender&&>(__sndr), - __receiver_t(this->__result_, __hcoro))) + : __sender_awaitable_base<__value_t, false>{__hcoro} + , __opstate_(STDEXEC::connect(static_cast<_Sender&&>(__sndr), __receiver_t(*this))) {} - constexpr void await_suspend(__std::coroutine_handle<_Promise>) noexcept + constexpr auto + await_suspend([[maybe_unused]] __std::coroutine_handle<_Promise> __hcoro) noexcept // + -> __std::coroutine_handle<> { + STDEXEC_ASSERT(this->__continuation_ == __hcoro); + + // Start the operation. STDEXEC::start(__opstate_); + + auto __expected = false; + if (this->__ready_.compare_exchange_strong(__expected, + true, + __std::memory_order_release, + __std::memory_order_acquire)) + { + // If __ready_ is still false, then the operation has not completed inline. The + // continuation will be resumed when the operation completes, so we return a + // noop_coroutine to suspend the current coroutine. + return __std::noop_coroutine(); + } + else + { + // The operation completed inline with set_value or set_error, so we can just + // resume the current coroutine. await_resume will either return the value or + // throw as appropriate. + return __hcoro; + } } private: @@ -252,18 +321,23 @@ namespace STDEXEC template requires __completes_inline<_Sender, env_of_t<_Promise&>> struct __sender_awaitable<_Promise, _Sender> - : __sender_awaitable_base<__detail::__value_t<_Sender, _Promise>> + : __sender_awaitable_base<__detail::__value_t<_Sender, _Promise>, true> { - constexpr explicit __sender_awaitable(_Sender&& sndr, __ignore) + using __value_t = __detail::__value_t<_Sender, _Promise>; + + constexpr explicit __sender_awaitable(_Sender&& sndr, + __std::coroutine_handle<_Promise> __hcoro) noexcept(__nothrow_move_constructible<_Sender>) - : __sndr_(static_cast<_Sender&&>(sndr)) + : __sender_awaitable_base<__value_t, true>{__hcoro} + , __sndr_(static_cast<_Sender&&>(sndr)) {} - bool await_suspend(__std::coroutine_handle<_Promise> __hcoro) + auto await_suspend([[maybe_unused]] __std::coroutine_handle<_Promise> __hcoro) + -> __std::coroutine_handle<> { + STDEXEC_ASSERT(this->__continuation_ == __hcoro); { - auto __opstate = STDEXEC::connect(static_cast<_Sender&&>(__sndr_), - __receiver_t(this->__result_, __hcoro)); + auto __opstate = STDEXEC::connect(static_cast<_Sender&&>(__sndr_), __receiver_t(*this)); // The following call to start will complete synchronously, writing its result // into the __result_ variant. STDEXEC::start(__opstate); @@ -275,18 +349,15 @@ namespace STDEXEC // unhandled_stopped() on the promise to propagate the stop signal. That will // result in the coroutine being torn down, so beware. We then resume the // returned coroutine handle (which may be a noop_coroutine). - __std::coroutine_handle<> __on_stopped = __hcoro.promise().unhandled_stopped(); - __on_stopped.resume(); - - // By returning true, we indicate that the coroutine should not be resumed - // (because it no longer exists). - return true; + return __hcoro.promise().unhandled_stopped(); + } + else + { + // The operation completed with set_value or set_error, so we can just resume + // the current coroutine. await_resume will either return the value or throw as + // appropriate. + return __hcoro; } - - // The operation completed with set_value or set_error, so we can just resume the - // current coroutine. await_resume with either return the value or throw as - // appropriate. - return false; } private: @@ -413,3 +484,5 @@ namespace STDEXEC inline constexpr as_awaitable_t as_awaitable{}; #endif } // namespace STDEXEC + +STDEXEC_PRAGMA_POP() From de92b16951c6fbd07e62c89ed3bbba4e69396005 Mon Sep 17 00:00:00 2001 From: Eric Niebler Date: Sun, 15 Mar 2026 19:10:17 -0700 Subject: [PATCH 02/11] review feedback --- include/stdexec/__detail/__as_awaitable.hpp | 9 +++++---- test/stdexec/types/test_task.cpp | 11 ++++++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/include/stdexec/__detail/__as_awaitable.hpp b/include/stdexec/__detail/__as_awaitable.hpp index 861d42ba1..1540ea48d 100644 --- a/include/stdexec/__detail/__as_awaitable.hpp +++ b/include/stdexec/__detail/__as_awaitable.hpp @@ -242,9 +242,10 @@ namespace STDEXEC private: void __done() noexcept { - // If __ready_ is still false, then we are completing inline. Update the - // value of __ready_. Otherwise, we are completing asynchronously, so we invoke - // the continuation without unwinding the stack. + // If __ready_ is still false when executing the CAS it means the started + // operation completed before await_suspend checked whether the operation + // completed. In this case resuming execution is handled by await_suspend. + // Otherwise, the execution needs to be resumed from here. auto __expected = false; auto& __awaiter = static_cast<__awaiter_t&>(this->__awaiter_); if (!__awaiter.__ready_.compare_exchange_strong(__expected, @@ -297,7 +298,7 @@ namespace STDEXEC __std::memory_order_release, __std::memory_order_acquire)) { - // If __ready_ is still false, then the operation has not completed inline. The + // If __ready_ is still false, then the operation did not complete inline. The // continuation will be resumed when the operation completes, so we return a // noop_coroutine to suspend the current coroutine. return __std::noop_coroutine(); diff --git a/test/stdexec/types/test_task.cpp b/test/stdexec/types/test_task.cpp index cb274886b..49bb83b00 100644 --- a/test/stdexec/types/test_task.cpp +++ b/test/stdexec/types/test_task.cpp @@ -244,6 +244,11 @@ namespace CHECK(i == 42); } + auto sync() -> ex::task + { + co_return 42; + } + auto nested() -> ex::task { auto sched = co_await ex::read_env(ex::get_scheduler); @@ -256,6 +261,10 @@ namespace { int result = co_await nested(); for (int i = 0; i < 1'000'000; ++i) + { + result += co_await sync(); + } + for (int i = 0; i < 1'000'000; ++i) { result += co_await ex::just(42); } @@ -266,7 +275,7 @@ namespace { auto t = test_task_awaits_inline_sndr_without_stack_overflow(); auto [i] = ex::sync_wait(std::move(t)).value(); - CHECK(i == 42'000'042); + CHECK(i == 84'000'042); } // FUTURE TODO: add support so that `co_await sndr` can return a reference. From fb3bf63a3be65d5e84c53cb8bd73a34359499c00 Mon Sep 17 00:00:00 2001 From: Eric Niebler Date: Mon, 16 Mar 2026 13:51:57 -0700 Subject: [PATCH 03/11] cleanup --- include/exec/at_coroutine_exit.hpp | 36 ++++++++++++---- include/exec/on_coro_disposition.hpp | 8 ++-- include/exec/task.hpp | 64 +++++++++++++++------------- 3 files changed, 65 insertions(+), 43 deletions(-) diff --git a/include/exec/at_coroutine_exit.hpp b/include/exec/at_coroutine_exit.hpp index c5b560844..e48a8547c 100644 --- a/include/exec/at_coroutine_exit.hpp +++ b/include/exec/at_coroutine_exit.hpp @@ -150,13 +150,23 @@ namespace experimental::execution return false; } + //! \brief Splice the cleanup action into the chain of continuations. + //! \param __parent The coroutine that is registering an action to be performed at + //! coroutine exit; i.e., the coroutine that is co_await-ing the result of calling + //! at_coroutine_exit. + //! \note This function is called directly from basic_task's await_transform, so + //! we have no guarantee that the __parent coroutine is suspended here. template <__has_continuation _Promise> auto await_suspend(__std::coroutine_handle<_Promise> __parent) noexcept -> bool { __coro_.promise().__scheduler_ = get_scheduler(get_env(__parent.promise())); + // This causes the parent to be resumed after the cleanup action is performed. __coro_.promise().set_continuation(__parent.promise().continuation()); + // This causes the parent to invoke the cleanup action when it performs the final + // suspend. Also, the parent is now responsible for destroying the cleanup + // coroutine. __parent.promise().set_continuation(__coro_); - return false; + return false; // i.e., do not suspend, call await_resume immediately } auto await_resume() noexcept -> std::tuple<_Ts&...> @@ -172,12 +182,13 @@ namespace experimental::execution return false; } - static auto - await_suspend(__std::coroutine_handle<__promise> __h) noexcept -> __std::coroutine_handle<> + //! \param __h The coroutine created by __co_impl below. + static auto await_suspend(__std::coroutine_handle<__promise> __h) noexcept // + -> __std::coroutine_handle<> { __promise& __p = __h.promise(); - auto __coro = __p.__is_unhandled_stopped_ ? __p.continuation().unhandled_stopped() - : __p.continuation().handle(); + auto __coro = __p.__is_stopped_ ? __p.continuation().unhandled_stopped() + : __p.continuation().handle(); return STDEXEC_DESTROY_AND_CONTINUE(__h, __coro); } @@ -209,11 +220,13 @@ namespace experimental::execution {} #endif + [[nodiscard]] auto initial_suspend() noexcept -> __std::suspend_always { return {}; } + [[nodiscard]] auto final_suspend() noexcept -> __final_awaitable { return {}; @@ -227,33 +240,38 @@ namespace experimental::execution std::terminate(); } + [[nodiscard]] auto unhandled_stopped() noexcept -> __std::coroutine_handle<__promise> { - __is_unhandled_stopped_ = true; + __is_stopped_ = true; return __std::coroutine_handle<__promise>::from_promise(*this); } + [[nodiscard]] auto get_return_object() noexcept -> __task { return __task(__std::coroutine_handle<__promise>::from_promise(*this)); } template + [[nodiscard]] auto await_transform(_Awaitable&& __awaitable) noexcept -> decltype(auto) { return as_awaitable(__die_on_stop(static_cast<_Awaitable&&>(__awaitable)), *this); } + [[nodiscard]] auto get_env() const noexcept -> __env { return {*this}; } - bool __is_unhandled_stopped_{false}; + bool __is_stopped_{false}; std::tuple<_Ts&...> __args_{}; __any_scheduler_t __scheduler_{STDEXEC::inline_scheduler{}}; }; + // __coro_ refers to the coroutine created by __co_impl below __std::coroutine_handle<__promise> __coro_; }; @@ -261,7 +279,7 @@ namespace experimental::execution { private: template - static auto __impl(_Action __action, _Ts... __ts) -> __task<_Ts...> + static auto __co_impl(_Action __action, _Ts... __ts) -> __task<_Ts...> { co_await static_cast<_Action&&>(__action)(static_cast<_Ts&&>(__ts)...); } @@ -271,7 +289,7 @@ namespace experimental::execution requires __callable<__decay_t<_Action>, __decay_t<_Ts>...> auto operator()(_Action&& __action, _Ts&&... __ts) const -> __task<_Ts...> { - return __impl(static_cast<_Action&&>(__action), static_cast<_Ts&&>(__ts)...); + return __co_impl(static_cast<_Action&&>(__action), static_cast<_Ts&&>(__ts)...); } }; } // namespace __at_coro_exit diff --git a/include/exec/on_coro_disposition.hpp b/include/exec/on_coro_disposition.hpp index c43c3868d..ef4d39f83 100644 --- a/include/exec/on_coro_disposition.hpp +++ b/include/exec/on_coro_disposition.hpp @@ -121,8 +121,8 @@ namespace experimental::execution await_suspend(__std::coroutine_handle<__promise> __h) noexcept -> __std::coroutine_handle<> { __promise& __p = __h.promise(); - auto __coro = __p.__is_unhandled_stopped_ ? __p.continuation().unhandled_stopped() - : __p.continuation().handle(); + auto __coro = __p.__is_stopped_ ? __p.continuation().unhandled_stopped() + : __p.continuation().handle(); return STDEXEC_DESTROY_AND_CONTINUE(__h, __coro); } @@ -174,7 +174,7 @@ namespace experimental::execution auto unhandled_stopped() noexcept -> __std::coroutine_handle<__promise> { - __is_unhandled_stopped_ = true; + __is_stopped_ = true; return __std::coroutine_handle<__promise>::from_promise(*this); } @@ -200,7 +200,7 @@ namespace experimental::execution return {*this}; } - bool __is_unhandled_stopped_{false}; + bool __is_stopped_{false}; std::tuple<_Ts&...> __args_{}; using __get_disposition_callback_t = task_disposition (*)(void*) noexcept; __std::coroutine_handle<> __parent_{}; diff --git a/include/exec/task.hpp b/include/exec/task.hpp index 7b16790f0..24af7e836 100644 --- a/include/exec/task.hpp +++ b/include/exec/task.hpp @@ -349,6 +349,12 @@ namespace experimental::execution class [[nodiscard]] basic_task { struct __promise; + + template + struct __task_awaiter; + + using __promise_context_t = _Context::template promise_context_t<__promise>; + public: using promise_type = __promise; @@ -356,6 +362,23 @@ namespace experimental::execution : __coro_(std::exchange(__that.__coro_, {})) {} + // Make this task awaitable within a particular context: + template + requires __std::constructible_from, + __promise_context_t&, + _ParentPromise&> + constexpr auto as_awaitable(_ParentPromise&) && noexcept -> __task_awaiter<_ParentPromise> + { + return __task_awaiter<_ParentPromise>{std::exchange(__coro_, {})}; + } + + // Make this task generally awaitable: + constexpr auto operator co_await() && noexcept -> __task_awaiter<> + requires __minvocable_q + { + return __task_awaiter<>{std::exchange(__coro_, {})}; + } + constexpr ~basic_task() { if (__coro_) @@ -382,8 +405,6 @@ namespace experimental::execution static constexpr void await_resume() noexcept {} }; - using __promise_context_t = _Context::template promise_context_t<__promise>; - struct __promise : __promise_base<_Ty> , with_awaitable_senders<__promise> @@ -423,19 +444,19 @@ namespace experimental::execution } #ifndef __clang_analyzer__ - template + template requires __scheduler_provider<_Context> - auto await_transform(_Awaitable&& __awaitable) noexcept -> decltype(auto) + auto await_transform(_CvSender&& __sndr) noexcept -> decltype(auto) { if constexpr (__completes_where_it_starts, + env_of_t<_CvSender>, __promise_context_t&>) { - return STDEXEC::as_awaitable(static_cast<_Awaitable&&>(__awaitable), *this); + return STDEXEC::as_awaitable(static_cast<_CvSender&&>(__sndr), *this); } else { - return STDEXEC::as_awaitable(continues_on(static_cast<_Awaitable&&>(__awaitable), + return STDEXEC::as_awaitable(continues_on(static_cast<_CvSender&&>(__sndr), get_scheduler(*__context_)), *this); } @@ -484,13 +505,10 @@ namespace experimental::execution bool __rescheduled_{false}; }; - template - struct __task_awaitable + template + struct __task_awaiter { - __std::coroutine_handle<__promise> __coro_; - __optional> __context_{}; - - constexpr ~__task_awaitable() + constexpr ~__task_awaiter() { if (__coro_) __coro_.destroy(); @@ -528,26 +546,12 @@ namespace experimental::execution if constexpr (!std::is_void_v<_Ty>) return std::move(__var::__get<0>(__coro_.promise().__data_)); } + + __std::coroutine_handle<__promise> __coro_; + __optional> __context_{}; }; public: - // Make this task awaitable within a particular context: - template - requires __std::constructible_from, - __promise_context_t&, - _ParentPromise&> - constexpr auto as_awaitable(_ParentPromise&) && noexcept -> __task_awaitable<_ParentPromise> - { - return __task_awaitable<_ParentPromise>{std::exchange(__coro_, {})}; - } - - // Make this task generally awaitable: - constexpr auto operator co_await() && noexcept -> __task_awaitable<> - requires __minvocable_q - { - return __task_awaitable<>{std::exchange(__coro_, {})}; - } - constexpr explicit basic_task(__std::coroutine_handle __coro) noexcept : __coro_(__coro) {} From 3b751da1e23cbd68ed1812e4d76f333929620f0d Mon Sep 17 00:00:00 2001 From: Eric Niebler Date: Mon, 16 Mar 2026 15:00:43 -0700 Subject: [PATCH 04/11] avoid GCC#94794 --- include/stdexec/__detail/__as_awaitable.hpp | 41 +++++++++++++------ .../__detail/__parallel_scheduler_backend.hpp | 1 + include/stdexec/__detail/__task_scheduler.hpp | 5 +++ test/stdexec/types/test_task.cpp | 5 +++ 4 files changed, 39 insertions(+), 13 deletions(-) diff --git a/include/stdexec/__detail/__as_awaitable.hpp b/include/stdexec/__detail/__as_awaitable.hpp index 1540ea48d..a09437d2e 100644 --- a/include/stdexec/__detail/__as_awaitable.hpp +++ b/include/stdexec/__detail/__as_awaitable.hpp @@ -30,6 +30,7 @@ #include #include // for std::identity #include +#include #include STDEXEC_PRAGMA_PUSH() @@ -133,7 +134,8 @@ namespace STDEXEC template struct __sender_awaitable_base<_Value, false> : __sender_awaitable_base<_Value, true> { - __std::atomic __ready_{false}; + __std::atomic __ready_{false}; + std::thread::id const __starting_thread_id_{std::this_thread::get_id()}; }; template @@ -246,16 +248,29 @@ namespace STDEXEC // operation completed before await_suspend checked whether the operation // completed. In this case resuming execution is handled by await_suspend. // Otherwise, the execution needs to be resumed from here. - auto __expected = false; - auto& __awaiter = static_cast<__awaiter_t&>(this->__awaiter_); - if (!__awaiter.__ready_.compare_exchange_strong(__expected, - true, - __std::memory_order_release, - __std::memory_order_acquire)) + auto& __awaiter = static_cast<__awaiter_t&>(this->__awaiter_); + + if (std::this_thread::get_id() != __awaiter.__starting_thread_id_) + { + // If we're completing on a different thread than the one that started the + // operation, we know we are completing asynchronously, so we need to resume + // the continuation from here. + __awaiter.__continuation_.resume(); + return; + } + + bool __expected = false; + bool const __was_ready = + !__awaiter.__ready_.compare_exchange_strong(__expected, + true, + __std::memory_order_release, + __std::memory_order_acquire); + if (__was_ready) { - // We get here if __ready_ was true. It got set to true in await_suspend() - // immediately after the operation was started, which implies that the operation - // completed asynchronously, so we need to resume the continuation. + // We get here if __ready_ was true then the CAS was executed. It got set to + // true in await_suspend() immediately after the operation was started, which + // implies that this completion is happening asynchronously, so we need to + // resume the continuation from here. __awaiter.__continuation_.resume(); } } @@ -298,9 +313,9 @@ namespace STDEXEC __std::memory_order_release, __std::memory_order_acquire)) { - // If __ready_ is still false, then the operation did not complete inline. The - // continuation will be resumed when the operation completes, so we return a - // noop_coroutine to suspend the current coroutine. + // If __ready_ is still false when executing the CAS, then the operation did not + // complete inline. The continuation will be resumed when the operation + // completes, so we return a noop_coroutine to suspend the current coroutine. return __std::noop_coroutine(); } else diff --git a/include/stdexec/__detail/__parallel_scheduler_backend.hpp b/include/stdexec/__detail/__parallel_scheduler_backend.hpp index 80627915e..502c89264 100644 --- a/include/stdexec/__detail/__parallel_scheduler_backend.hpp +++ b/include/stdexec/__detail/__parallel_scheduler_backend.hpp @@ -35,6 +35,7 @@ STDEXEC_PRAGMA_PUSH() STDEXEC_PRAGMA_IGNORE_MSVC(4702) // warning C4702: unreachable code +STDEXEC_PRAGMA_IGNORE_GNU("-Warray-bounds") namespace STDEXEC { diff --git a/include/stdexec/__detail/__task_scheduler.hpp b/include/stdexec/__detail/__task_scheduler.hpp index 82e283088..a8203aac3 100644 --- a/include/stdexec/__detail/__task_scheduler.hpp +++ b/include/stdexec/__detail/__task_scheduler.hpp @@ -38,6 +38,9 @@ #include #include +STDEXEC_PRAGMA_PUSH() +STDEXEC_PRAGMA_IGNORE_GNU("-Warray-bounds") + namespace STDEXEC { class task_scheduler; @@ -779,3 +782,5 @@ namespace STDEXEC } } // namespace __detail } // namespace STDEXEC + +STDEXEC_PRAGMA_POP() diff --git a/test/stdexec/types/test_task.cpp b/test/stdexec/types/test_task.cpp index 49bb83b00..4e3bbd59b 100644 --- a/test/stdexec/types/test_task.cpp +++ b/test/stdexec/types/test_task.cpp @@ -244,6 +244,9 @@ namespace CHECK(i == 42); } +# if !STDEXEC_GCC() || defined(__OPTIMIZE__) + // This test is disabled on GCC due to https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94794 + auto sync() -> ex::task { co_return 42; @@ -278,6 +281,8 @@ namespace CHECK(i == 84'000'042); } +# endif + // FUTURE TODO: add support so that `co_await sndr` can return a reference. // auto test_task_awaits_just_ref_sender() -> ex::task { // int value = 42; From 3f1f04c12b4aca16afe17da0844e6cac9dbbe4c4 Mon Sep 17 00:00:00 2001 From: Eric Niebler Date: Mon, 16 Mar 2026 15:21:35 -0700 Subject: [PATCH 05/11] more work trying to make gcc happy --- .github/workflows/ci.cpu.yml | 5 ++++- test/stdexec/types/test_task.cpp | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.cpu.yml b/.github/workflows/ci.cpu.yml index b796eb809..88475e0d0 100644 --- a/.github/workflows/ci.cpu.yml +++ b/.github/workflows/ci.cpu.yml @@ -27,7 +27,10 @@ jobs: - { name: "CPU (gcc 11, Release)", build: "Release", tag: gcc11-cuda12.9, cxxstd: "20", cxxflags: "", } - { name: "CPU (gcc 11, Release, ASAN)", build: "Release", tag: gcc11-cuda12.9, cxxstd: "20", cxxflags: "-fsanitize=address" } - { name: "CPU (gcc 12, Release, TSAN)", build: "Release", tag: gcc12-cuda12.9, cxxstd: "20", cxxflags: "-fsanitize=thread" } - - { name: "CPU (gcc 13, Debug)", build: "Release", tag: gcc13-cuda12.9, cxxstd: "20", cxxflags: "", } + - { name: "CPU (gcc 13, Debug)", build: "Debug", tag: gcc13-cuda12.9, cxxstd: "20", cxxflags: "", } + - { name: "CPU (gcc 14, Debug)", build: "Debug", tag: gcc14-cuda12.9, cxxstd: "20", cxxflags: "", } + - { name: "CPU (gcc 14, Debug, ASAN)", build: "Debug", tag: gcc14-cuda12.9, cxxstd: "20", cxxflags: "-fsanitize=address" } + - { name: "CPU (gcc 14, Debug, TSAN)", build: "Debug", tag: gcc14-cuda12.9, cxxstd: "20", cxxflags: "-fsanitize=thread" } - { name: "CPU (gcc 14, Release, LEAK)", build: "Release", tag: gcc14-cuda12.9, cxxstd: "20", cxxflags: "-fsanitize=leak", } - { name: "CPU (gcc 14, Release, c++23)", build: "Release", tag: gcc14-cuda12.9, cxxstd: "23", cxxflags: "", } container: diff --git a/test/stdexec/types/test_task.cpp b/test/stdexec/types/test_task.cpp index 4e3bbd59b..c84b1840e 100644 --- a/test/stdexec/types/test_task.cpp +++ b/test/stdexec/types/test_task.cpp @@ -244,7 +244,7 @@ namespace CHECK(i == 42); } -# if !STDEXEC_GCC() || defined(__OPTIMIZE__) +# if !STDEXEC_GCC() || (STDEXEC_GCC_VERSION >= 13'00 && defined(__OPTIMIZE__)) // This test is disabled on GCC due to https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94794 auto sync() -> ex::task From d10e32622ab9f716dacc91be0d72be4ae4bd9def Mon Sep 17 00:00:00 2001 From: Eric Niebler Date: Mon, 16 Mar 2026 15:36:45 -0700 Subject: [PATCH 06/11] link examples to TBB because of libstdc++ PSTL --- examples/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index ea0896041..4d55ad07c 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -30,7 +30,8 @@ function(def_example example) add_executable(${target} ${source}) target_link_libraries(${target} PRIVATE STDEXEC::stdexec - stdexec_executable_flags) + stdexec_executable_flags + $) endfunction() set(stdexec_examples From c2ed4a8d33aee915652400dce11e0dd070751655 Mon Sep 17 00:00:00 2001 From: Eric Niebler Date: Mon, 16 Mar 2026 15:44:16 -0700 Subject: [PATCH 07/11] link TBB to the tests --- .github/workflows/ci.cpu.yml | 1 + test/CMakeLists.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/ci.cpu.yml b/.github/workflows/ci.cpu.yml index 88475e0d0..947577859 100644 --- a/.github/workflows/ci.cpu.yml +++ b/.github/workflows/ci.cpu.yml @@ -21,6 +21,7 @@ jobs: include: - { name: "CPU (clang 16, Debug)", build: "Debug", tag: llvm16-cuda12.9, cxxstd: "20", cxxflags: "-stdlib=libc++" } - { name: "CPU (clang 16, Debug, c++23)", build: "Debug", tag: llvm16-cuda12.9, cxxstd: "23", cxxflags: "-stdlib=libc++" } + - { name: "CPU (clang 16, Debug, TSAN)", build: "Debug", tag: llvm16-cuda12.9, cxxstd: "20", cxxflags: "-fsanitize=thread" } - { name: "CPU (clang 16, Release)", build: "Release", tag: llvm16-cuda12.9, cxxstd: "20", cxxflags: "-stdlib=libc++" } - { name: "CPU (clang 16, Release, ASAN)", build: "Release", tag: llvm16-cuda12.9, cxxstd: "20", cxxflags: "-stdlib=libc++ -fsanitize=address -fsanitize-ignorelist=/home/coder/stdexec/sanitizer-ignorelist.txt" } - { name: "CPU (gcc 11, Debug)", build: "Debug", tag: gcc11-cuda12.9, cxxstd: "20", cxxflags: "", } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7d0eb9dfb..e7cd6da71 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -86,6 +86,7 @@ set_target_properties(common_test_settings PROPERTIES ) target_include_directories(common_test_settings INTERFACE "${CMAKE_CURRENT_LIST_DIR}") target_compile_definitions(common_test_settings INTERFACE STDEXEC_NAMESPACE=std::execution) +target_link_libraries(common_test_settings INTERFACE $) # target_compile_definitions( # common_test_settings INTERFACE # $<$,$>>:STDEXEC_ENABLE_EXTRA_TYPE_CHECKING>) From 40607f51385e7588bf403bbf94244cf9404b857f Mon Sep 17 00:00:00 2001 From: Eric Niebler Date: Mon, 16 Mar 2026 18:23:24 -0700 Subject: [PATCH 08/11] fix race in `reschedule_coroutine_on` --- examples/CMakeLists.txt | 28 ++++--- include/exec/at_coroutine_exit.hpp | 3 +- include/exec/task.hpp | 120 ++++++++++++++++++++++++----- 3 files changed, 119 insertions(+), 32 deletions(-) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 4d55ad07c..60db139d8 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -64,30 +64,34 @@ if (LINUX) endif () foreach(example ${stdexec_examples}) - def_example(${example}) + def_example(${example}) endforeach() if(STDEXEC_ENABLE_CUDA) - add_subdirectory(nvexec) + add_subdirectory(nvexec) endif() if (STDEXEC_ENABLE_TBB) - add_executable(example.benchmark.tbb_thread_pool benchmark/tbb_thread_pool.cpp) - target_link_libraries(example.benchmark.tbb_thread_pool PRIVATE STDEXEC::tbbexec) + add_executable(example.benchmark.tbb_thread_pool benchmark/tbb_thread_pool.cpp) + target_link_libraries(example.benchmark.tbb_thread_pool PRIVATE STDEXEC::tbbexec) - add_executable(example.benchmark.tbb_thread_pool_nested benchmark/tbb_thread_pool_nested.cpp) - target_link_libraries(example.benchmark.tbb_thread_pool_nested PRIVATE STDEXEC::tbbexec) + add_executable(example.benchmark.tbb_thread_pool_nested benchmark/tbb_thread_pool_nested.cpp) + target_link_libraries(example.benchmark.tbb_thread_pool_nested PRIVATE STDEXEC::tbbexec) - add_executable(example.benchmark.fibonacci benchmark/fibonacci.cpp) - target_link_libraries(example.benchmark.fibonacci PRIVATE STDEXEC::tbbexec) + add_executable(example.benchmark.fibonacci benchmark/fibonacci.cpp) + target_link_libraries(example.benchmark.fibonacci PRIVATE STDEXEC::tbbexec) endif() if(STDEXEC_ENABLE_TASKFLOW) - add_executable(example.benchmark.taskflow_thread_pool benchmark/taskflow_thread_pool.cpp) - target_link_libraries(example.benchmark.taskflow_thread_pool PRIVATE STDEXEC::taskflowexec) + add_executable(example.benchmark.taskflow_thread_pool benchmark/taskflow_thread_pool.cpp) + target_link_libraries(example.benchmark.taskflow_thread_pool + PRIVATE STDEXEC::taskflowexec + $) endif() if(STDEXEC_ENABLE_ASIO) -add_executable(example.benchmark.asio_thread_pool benchmark/asio_thread_pool.cpp) -target_link_libraries(example.benchmark.asio_thread_pool PRIVATE STDEXEC::asioexec) + add_executable(example.benchmark.asio_thread_pool benchmark/asio_thread_pool.cpp) + target_link_libraries(example.benchmark.asio_thread_pool + PRIVATE STDEXEC::asioexec + $) endif() diff --git a/include/exec/at_coroutine_exit.hpp b/include/exec/at_coroutine_exit.hpp index e48a8547c..9a01032eb 100644 --- a/include/exec/at_coroutine_exit.hpp +++ b/include/exec/at_coroutine_exit.hpp @@ -154,11 +154,10 @@ namespace experimental::execution //! \param __parent The coroutine that is registering an action to be performed at //! coroutine exit; i.e., the coroutine that is co_await-ing the result of calling //! at_coroutine_exit. - //! \note This function is called directly from basic_task's await_transform, so - //! we have no guarantee that the __parent coroutine is suspended here. template <__has_continuation _Promise> auto await_suspend(__std::coroutine_handle<_Promise> __parent) noexcept -> bool { + // Set the cleanup task's scheduler to the parent coroutine's scheduler. __coro_.promise().__scheduler_ = get_scheduler(get_env(__parent.promise())); // This causes the parent to be resumed after the cleanup action is performed. __coro_.promise().set_continuation(__parent.promise().continuation()); diff --git a/include/exec/task.hpp b/include/exec/task.hpp index 24af7e836..e4c08fc2a 100644 --- a/include/exec/task.hpp +++ b/include/exec/task.hpp @@ -24,6 +24,7 @@ #include "../stdexec/__detail/__optional.hpp" #include "../stdexec/__detail/__variant.hpp" #include "../stdexec/execution.hpp" +#include "../stdexec/functional.hpp" #include "any_sender_of.hpp" #include "at_coroutine_exit.hpp" @@ -328,16 +329,111 @@ namespace experimental::execution failed, }; - struct __reschedule_coroutine_on + template + struct __reschedule_receiver + { + using receiver_concept = receiver_t; + + void set_value() noexcept + { + // Resuming the continuation of the parent coroutine will cause it to continue + // executing on the new scheduler. + __parent_.resume(); + } + + void set_error(std::exception_ptr __eptr) noexcept + { + __eptr_ = std::move(__eptr); + __parent_.resume(); + } + + void set_stopped() noexcept + { + // Resuming the stopped continuation unwinds the coroutine stack until we reach + // a promise that can handle the stopped signal. The coroutine referred to by + // __continuation_ will never be resumed. + __std::coroutine_handle<> __unwind = __parent_.promise().unhandled_stopped(); + __unwind.resume(); + } + + [[nodiscard]] + auto get_env() const noexcept -> env_of_t<_Promise> + { + return STDEXEC::get_env(__parent_.promise()); + } + + std::exception_ptr& __eptr_; + __std::coroutine_handle<_Promise> __parent_; + }; + + template + struct __reschedule_awaiter + { + using __sender_t = __result_of>; + using __receiver_t = __reschedule_receiver<_Promise>; + using __opstate_t = STDEXEC::connect_result_t<__sender_t, __receiver_t>; + + static constexpr auto await_ready() noexcept -> bool + { + return false; + } + + auto await_suspend(__std::coroutine_handle<_Promise> __h) noexcept -> bool + { + STDEXEC_TRY + { + auto& __p = __h.promise(); + + if (!std::exchange(__p.__rescheduled_, true)) + { + // Create a cleanup action that transitions back onto the current scheduler: + auto __sched = get_scheduler(*__p.__context_); + auto __guard = at_coroutine_exit(__compose(unstoppable, STDEXEC::schedule), + std::move(__sched)); + // Insert the cleanup action into the head of the continuation chain by + // making direct calls to the cleanup task's awaiter member functions. See + // type __at_coro_exit::__task in at_coroutine_exit.hpp: + __guard.await_suspend(__h); + (void) __guard.await_resume(); + } + + __p.__context_->set_scheduler(__new_sched_); + auto& __op = __opstate_.__emplace_from(STDEXEC::connect, + unstoppable(schedule(__new_sched_)), + __receiver_t{__eptr_, __h}); + STDEXEC::start(__op); + return true; // suspend the coroutine until the scheduler operation completes + } + STDEXEC_CATCH_ALL + { + __eptr_ = std::current_exception(); + return false; + } + } + + void await_resume() noexcept + { + if (__eptr_) + { + std::rethrow_exception(std::move(__eptr_)); + } + } + + _Scheduler __new_sched_; + std::exception_ptr __eptr_; + __optional<__opstate_t> __opstate_; + }; + + struct __reschedule_coroutine_on_t { template - struct __wrap + struct __wrapper { _Scheduler __sched_; }; template - constexpr auto operator()(_Scheduler __sched) const noexcept -> __wrap<_Scheduler> + constexpr auto operator()(_Scheduler __sched) const noexcept -> __wrapper<_Scheduler> { return {static_cast<_Scheduler&&>(__sched)}; } @@ -464,22 +560,10 @@ namespace experimental::execution template requires __scheduler_provider<_Context> - auto await_transform(__reschedule_coroutine_on::__wrap<_Scheduler> __box) noexcept + auto await_transform(__reschedule_coroutine_on_t::__wrapper<_Scheduler> __box) noexcept -> decltype(auto) { - if (!std::exchange(__rescheduled_, true)) - { - // Create a cleanup action that transitions back onto the current scheduler: - auto __sched = get_scheduler(*__context_); - auto __cleanup_task = at_coroutine_exit(schedule, std::move(__sched)); - // Insert the cleanup action into the head of the continuation chain by making - // direct calls to the cleanup task's awaiter member functions. See type - // __at_coro_exit::__task in at_coroutine_exit.hpp: - __cleanup_task.await_suspend(__std::coroutine_handle<__promise>::from_promise(*this)); - (void) __cleanup_task.await_resume(); - } - __context_->set_scheduler(__box.__sched_); - return STDEXEC::as_awaitable(schedule(__box.__sched_), *this); + return __reschedule_awaiter<_Scheduler, __promise>{__box.__sched_}; } #endif @@ -574,7 +658,7 @@ namespace experimental::execution template using task = basic_task<_Ty, default_task_context<_Ty>>; - inline constexpr __task::__reschedule_coroutine_on reschedule_coroutine_on{}; + inline constexpr __task::__reschedule_coroutine_on_t reschedule_coroutine_on{}; } // namespace experimental::execution namespace exec = experimental::execution; From 0e94128b24cc26f534068d7f4ddceaf3b858490e Mon Sep 17 00:00:00 2001 From: Eric Niebler Date: Mon, 16 Mar 2026 19:06:24 -0700 Subject: [PATCH 09/11] defer destruction of the `at_coroutine_exit` coroutine --- README.md | 1 + include/exec/at_coroutine_exit.hpp | 21 ++--- include/exec/on_coro_disposition.hpp | 16 ++-- include/stdexec/coroutine.hpp | 119 --------------------------- 4 files changed, 20 insertions(+), 137 deletions(-) diff --git a/README.md b/README.md index 612723e4a..dadaef2fe 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ dependencies and only requires a sufficiently new compiler: - gcc 11+ - clang 16+ +- MSVC 14.43+ - XCode 16+ - [nvc++ 25.9+](https://developer.nvidia.com/nvidia-hpc-sdk-releases) (required for [GPU support](#gpu-support)). diff --git a/include/exec/at_coroutine_exit.hpp b/include/exec/at_coroutine_exit.hpp index 9a01032eb..393eb5a3b 100644 --- a/include/exec/at_coroutine_exit.hpp +++ b/include/exec/at_coroutine_exit.hpp @@ -144,6 +144,12 @@ namespace experimental::execution : __coro_(std::exchange(__that.__coro_, {})) {} + ~__task() + { + if (__coro_) + __coro_.destroy(); + } + [[nodiscard]] static constexpr auto await_ready() noexcept -> bool { @@ -185,10 +191,8 @@ namespace experimental::execution static auto await_suspend(__std::coroutine_handle<__promise> __h) noexcept // -> __std::coroutine_handle<> { - __promise& __p = __h.promise(); - auto __coro = __p.__is_stopped_ ? __p.continuation().unhandled_stopped() - : __p.continuation().handle(); - return STDEXEC_DESTROY_AND_CONTINUE(__h, __coro); + auto __cont = __h.promise().continuation(); + return __h.promise().__is_stopped_ ? __cont.unhandled_stopped() : __cont.handle(); } void await_resume() const noexcept {} @@ -207,17 +211,10 @@ namespace experimental::execution struct __promise : with_awaitable_senders<__promise> { -#if STDEXEC_EDG() template - __promise(_Action&&, _Ts&&... __ts) noexcept + explicit(!STDEXEC_EDG()) __promise(_Action&&, _Ts&... __ts) noexcept : __args_{__ts...} {} -#else - template - explicit __promise(_Action&&, _Ts&... __ts) noexcept - : __args_{__ts...} - {} -#endif [[nodiscard]] auto initial_suspend() noexcept -> __std::suspend_always diff --git a/include/exec/on_coro_disposition.hpp b/include/exec/on_coro_disposition.hpp index ef4d39f83..173ed3dd1 100644 --- a/include/exec/on_coro_disposition.hpp +++ b/include/exec/on_coro_disposition.hpp @@ -83,6 +83,12 @@ namespace experimental::execution : __coro_(std::exchange(__that.__coro_, {})) {} + ~__task() + { + if (__coro_) + __coro_.destroy(); + } + [[nodiscard]] auto await_ready() const noexcept -> bool { @@ -117,13 +123,11 @@ namespace experimental::execution return false; } - static auto - await_suspend(__std::coroutine_handle<__promise> __h) noexcept -> __std::coroutine_handle<> + static auto await_suspend(__std::coroutine_handle<__promise> __h) noexcept // + -> __std::coroutine_handle<> { - __promise& __p = __h.promise(); - auto __coro = __p.__is_stopped_ ? __p.continuation().unhandled_stopped() - : __p.continuation().handle(); - return STDEXEC_DESTROY_AND_CONTINUE(__h, __coro); + auto __cont = __h.promise().continuation(); + return __h.promise().__is_stopped_ ? __cont.unhandled_stopped() : __cont.handle(); } void await_resume() const noexcept {} diff --git a/include/stdexec/coroutine.hpp b/include/stdexec/coroutine.hpp index 9c1fa968d..c6cb69dce 100644 --- a/include/stdexec/coroutine.hpp +++ b/include/stdexec/coroutine.hpp @@ -17,122 +17,3 @@ #include "__detail/__awaitable.hpp" // IWYU pragma: export #include "__detail/__config.hpp" - -#if STDEXEC_MSVC() && STDEXEC_MSVC_VERSION <= 19'39 -namespace STDEXEC -{ - // MSVCBUG https://developercommunity.visualstudio.com/t/destroy-coroutine-from-final_suspend-r/10096047 - - // Prior to Visual Studio 17.9 (Feb, 2024), aka MSVC 19.39, MSVC incorrectly allocates the return - // buffer for await_suspend calls within the suspended coroutine frame. When the suspended - // coroutine is destroyed within await_suspend, the continuation coroutine handle is not only used - // after free, but also overwritten by the debug malloc implementation when NRVO is in play. - - // This workaround delays the destruction of the suspended coroutine by wrapping the continuation - // in another coroutine which destroys the former and transfers execution to the original - // continuation. - - // The wrapping coroutine is thread-local and is reused within the thread for each - // destroy-and-continue sequence. The wrapping coroutine itself is destroyed at thread exit. - - namespace __destroy_and_continue_msvc - { - struct __task - { - struct promise_type - { - __task get_return_object() noexcept - { - return {__std::coroutine_handle::from_promise(*this)}; - } - - static std::suspend_never initial_suspend() noexcept - { - return {}; - } - - static std::suspend_never final_suspend() noexcept - { - STDEXEC_ASSERT(!"Should never get here"); - return {}; - } - - static void return_void() noexcept - { - STDEXEC_ASSERT(!"Should never get here"); - } - - static void unhandled_exception() noexcept - { - STDEXEC_ASSERT(!"Should never get here"); - } - }; - - __std::coroutine_handle<> __coro_; - }; - - struct __continue_t - { - static constexpr bool await_ready() noexcept - { - return false; - } - - __std::coroutine_handle<> await_suspend(__std::coroutine_handle<>) noexcept - { - return __continue_; - } - - static void await_resume() noexcept {} - - __std::coroutine_handle<> __continue_; - }; - - struct __context - { - __std::coroutine_handle<> __destroy_; - __std::coroutine_handle<> __continue_; - }; - - inline __task __co_impl(__context& __c) - { - while (true) - { - co_await __continue_t{__c.__continue_}; - __c.__destroy_.destroy(); - } - } - - struct __context_and_coro - { - __context_and_coro() - { - __context_.__continue_ = __std::noop_coroutine(); - __coro_ = __co_impl(__context_).__coro_; - } - - ~__context_and_coro() - { - __coro_.destroy(); - } - - __context __context_; - __std::coroutine_handle<> __coro_; - }; - - inline __std::coroutine_handle<> - __impl(__std::coroutine_handle<> __destroy, __std::coroutine_handle<> __continue) - { - static thread_local __context_and_coro __c; - __c.__context_.__destroy_ = __destroy; - __c.__context_.__continue_ = __continue; - return __c.__coro_; - } - } // namespace __destroy_and_continue_msvc -} // namespace STDEXEC - -# define STDEXEC_DESTROY_AND_CONTINUE(__destroy, __continue) \ - (::STDEXEC::__destroy_and_continue_msvc::__impl(__destroy, __continue)) -#else -# define STDEXEC_DESTROY_AND_CONTINUE(__destroy, __continue) (__destroy.destroy(), __continue) -#endif From 0afdbcd03a3808847282eb6ddbd8ac38b594c099 Mon Sep 17 00:00:00 2001 From: Eric Niebler Date: Mon, 16 Mar 2026 19:23:39 -0700 Subject: [PATCH 10/11] actually destroy the reschedule-coroutine coroutine --- include/exec/at_coroutine_exit.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/exec/at_coroutine_exit.hpp b/include/exec/at_coroutine_exit.hpp index 393eb5a3b..47a8d6bc3 100644 --- a/include/exec/at_coroutine_exit.hpp +++ b/include/exec/at_coroutine_exit.hpp @@ -176,7 +176,7 @@ namespace experimental::execution auto await_resume() noexcept -> std::tuple<_Ts&...> { - return std::exchange(__coro_, {}).promise().__args_; + return __coro_.promise().__args_; } private: From 834dd461028d68bbc1d2b4c520c2483eb93f8fc0 Mon Sep 17 00:00:00 2001 From: Eric Niebler Date: Mon, 16 Mar 2026 20:18:23 -0700 Subject: [PATCH 11/11] wow yeah ok, THAT didn't work --- include/exec/at_coroutine_exit.hpp | 20 +++++--------------- include/exec/on_coro_disposition.hpp | 10 +++------- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/include/exec/at_coroutine_exit.hpp b/include/exec/at_coroutine_exit.hpp index 47a8d6bc3..a07635880 100644 --- a/include/exec/at_coroutine_exit.hpp +++ b/include/exec/at_coroutine_exit.hpp @@ -130,26 +130,14 @@ namespace experimental::execution public: using promise_type = __promise; -#if STDEXEC_EDG() - __task(__std::coroutine_handle<__promise> __coro) noexcept + explicit(!STDEXEC_EDG()) __task(__std::coroutine_handle<__promise> __coro) noexcept : __coro_(__coro) {} -#else - explicit __task(__std::coroutine_handle<__promise> __coro) noexcept - : __coro_(__coro) - {} -#endif __task(__task&& __that) noexcept : __coro_(std::exchange(__that.__coro_, {})) {} - ~__task() - { - if (__coro_) - __coro_.destroy(); - } - [[nodiscard]] static constexpr auto await_ready() noexcept -> bool { @@ -176,7 +164,7 @@ namespace experimental::execution auto await_resume() noexcept -> std::tuple<_Ts&...> { - return __coro_.promise().__args_; + return std::exchange(__coro_, {}).promise().__args_; } private: @@ -192,7 +180,9 @@ namespace experimental::execution -> __std::coroutine_handle<> { auto __cont = __h.promise().continuation(); - return __h.promise().__is_stopped_ ? __cont.unhandled_stopped() : __cont.handle(); + auto __coro = __h.promise().__is_stopped_ ? __cont.unhandled_stopped() : __cont.handle(); + __h.destroy(); + return __coro; } void await_resume() const noexcept {} diff --git a/include/exec/on_coro_disposition.hpp b/include/exec/on_coro_disposition.hpp index 173ed3dd1..95286577e 100644 --- a/include/exec/on_coro_disposition.hpp +++ b/include/exec/on_coro_disposition.hpp @@ -83,12 +83,6 @@ namespace experimental::execution : __coro_(std::exchange(__that.__coro_, {})) {} - ~__task() - { - if (__coro_) - __coro_.destroy(); - } - [[nodiscard]] auto await_ready() const noexcept -> bool { @@ -127,7 +121,9 @@ namespace experimental::execution -> __std::coroutine_handle<> { auto __cont = __h.promise().continuation(); - return __h.promise().__is_stopped_ ? __cont.unhandled_stopped() : __cont.handle(); + auto __coro = __h.promise().__is_stopped_ ? __cont.unhandled_stopped() : __cont.handle(); + __h.destroy(); + return __coro; } void await_resume() const noexcept {}