From a50f49d1007826bc317e3ea384b3695aa95881b7 Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Sat, 1 Aug 2026 11:20:05 -0400 Subject: [PATCH 1/3] exec::asio::completion_token: Use STDEXEC Invocation Utilities --- include/exec/asio/completion_token.hpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/include/exec/asio/completion_token.hpp b/include/exec/asio/completion_token.hpp index f8ad7b2cb..a7af06ab9 100644 --- a/include/exec/asio/completion_token.hpp +++ b/include/exec/asio/completion_token.hpp @@ -27,12 +27,12 @@ #include "../../stdexec/__detail/__queries.hpp" #include "../../stdexec/__detail/__receivers.hpp" #include "../../stdexec/__detail/__type_traits.hpp" +#include "../../stdexec/functional.hpp" #include "../../stdexec/stop_token.hpp" #include "as_default_on.hpp" #include #include -#include #include #include #include @@ -377,9 +377,9 @@ namespace experimental::execution::asio std::apply( [&](auto&&... args) { - std::invoke(static_cast(init_), - completion_handler(*this), - static_cast(args)...); + ::STDEXEC::__invoke(static_cast(init_), + completion_handler(*this), + static_cast(args)...); }, std::move(args_)); } @@ -618,7 +618,7 @@ namespace ASIOEXEC_ASIO_NAMESPACE }; template - requires requires(Receiver const & r) { ::STDEXEC::get_allocator(::STDEXEC::get_env(r)); } + requires ::STDEXEC::__callable<::STDEXEC::get_allocator_t, ::STDEXEC::env_of_t> struct associated_allocator< ::exec::asio::detail::completion_token::completion_handler, Allocator> From d9d52c2635db83158a072af48bc1a25c38229e5e Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Fri, 13 Feb 2026 15:59:18 -0500 Subject: [PATCH 2/3] exec::asio::completion_token: Remove std::unique_lock From "Frame" Reduces the size of a "frame" by removing the std::unique_lock member variable and making the "frame" itself the lock guard. This is the lock management method shown when presenting exec::asio::completion_token in the CppCon 2025 talk "std::execution in Asio Codebases: Adopting Senders Without a Rewrite." --- include/exec/asio/completion_token.hpp | 52 ++++++++++++++------------ 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/include/exec/asio/completion_token.hpp b/include/exec/asio/completion_token.hpp index a7af06ab9..4fbead55f 100644 --- a/include/exec/asio/completion_token.hpp +++ b/include/exec/asio/completion_token.hpp @@ -179,13 +179,11 @@ namespace experimental::execution::asio class frame_ { - operation_state_base& self_; - std::unique_lock l_; - frame_* prev_; + operation_state_base* self_; + frame_* prev_; public: explicit frame_(operation_state_base& self) noexcept - : self_(self) - , l_(self.m_) + : self_((self.m_.lock(), &self)) , prev_(self.frames_) { self.frames_ = this; @@ -195,23 +193,24 @@ namespace experimental::execution::asio ~frame_() noexcept { - if (l_) + if (self_) { - STDEXEC_ASSERT(self_.frames_ == this); - self_.frames_ = prev_; - if (!self_.frames_ && self_.abandoned_) + std::unique_lock l(self_->m_, std::adopt_lock); + STDEXEC_ASSERT(self_->frames_ == this); + self_->frames_ = prev_; + if (!self_->frames_ && self_->abandoned_) { // We are the last frame and the handler is gone so it's up to us to // finalize the operation - l_.unlock(); - self_.callback_.reset(); - if (self_.ex_) + l.unlock(); + self_->callback_.reset(); + if (self_->ex_) { - ::STDEXEC::set_error(static_cast(self_.r_), std::move(self_.ex_)); + ::STDEXEC::set_error(static_cast(self_->r_), std::move(self_->ex_)); } else { - ::STDEXEC::set_stopped(static_cast(self_.r_)); + ::STDEXEC::set_stopped(static_cast(self_->r_)); } } } @@ -219,22 +218,27 @@ namespace experimental::execution::asio explicit operator bool() const noexcept { - return bool(l_); + return bool(self_); } void release() noexcept { - auto ptr = this; - do + auto&& self = *self_; + STDEXEC_ASSERT(this == self.frames_); + for (;;) { - STDEXEC_ASSERT(ptr->l_); - STDEXEC_ASSERT(self_.frames_ == ptr); - ptr = ptr->prev_; - self_.frames_->l_.unlock(); - self_.frames_->prev_ = nullptr; - self_.frames_ = ptr; + STDEXEC_ASSERT(self.frames_); + STDEXEC_ASSERT(self.frames_->self_ == &self); + auto const current = std::exchange(self.frames_, self.frames_->prev_); + current->self_ = nullptr; + current->prev_ = nullptr; + self.m_.unlock(); + if (!self.frames_) + { + break; + } } - while (ptr); + STDEXEC_ASSERT(!self_); } }; From 6ffbd0ac4a1c795fe85a935c932746f10fc460c7 Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Fri, 13 Feb 2026 16:03:28 -0500 Subject: [PATCH 3/3] exec::asio::thread_unsafe_completion_token & _use_sender For general purpose (i.e. potentially multithreaded) use the asynchronous operations which result when passing the exec::asio:: completion_token and ::use_sender completion tokens must use a recursive mutex internally. However if the user knows that no multithreaded use will occur this recursive mutex is pure overhead. Provided the exec::asio::thread_unsafe_completion_token and _use_sender completion tokens which do not make use of a recursive mutex for the aforementioned use case. --- include/asioexec/completion_token.hpp | 15 +++ include/asioexec/use_sender.hpp | 11 ++ include/exec/asio/completion_token.hpp | 142 +++++++++++++---------- include/exec/asio/use_sender.hpp | 31 +++-- test/exec/asio/test_completion_token.cpp | 2 +- test/exec/asio/test_use_sender.cpp | 2 +- 6 files changed, 129 insertions(+), 74 deletions(-) diff --git a/include/asioexec/completion_token.hpp b/include/asioexec/completion_token.hpp index 36bbba072..cfb8cbda7 100644 --- a/include/asioexec/completion_token.hpp +++ b/include/asioexec/completion_token.hpp @@ -36,9 +36,24 @@ namespace asioexec "exec::asio::completion_token_t " "instead.")]] = exec::asio::completion_token_t; + using thread_unsafe_completion_token_t + [[deprecated("asioexec::thread_unsafe_completion_token_t " + "is deprecated. Please use " + "exec::asio::thread_unsafe_completion_token_" + "t instead.")]] = exec::asio::thread_unsafe_completion_token_t; + inline constexpr auto const & completion_token [[deprecated("asioexec::completion_token is " "deprecated. Please use " "exec::asio::completion_token " "instead.")]] = exec::asio::completion_token; + + inline constexpr auto const & thread_unsafe_completion_token [[deprecated("asioexec::thread_" + "unsafe_completion_" + "token is deprecated. " + "Please use " + "exec::asio::thread_" + "unsafe_completion_" + "token instead.")]] + = exec::asio::thread_unsafe_completion_token; } // namespace asioexec diff --git a/include/asioexec/use_sender.hpp b/include/asioexec/use_sender.hpp index 9883b9b95..4008e3e4f 100644 --- a/include/asioexec/use_sender.hpp +++ b/include/asioexec/use_sender.hpp @@ -36,8 +36,19 @@ namespace asioexec "exec::asio::use_sender_t " "instead.")]] = exec::asio::use_sender_t; + using thread_unsafe_use_sender_t [[deprecated( + "asioexec::thread_unsafe_use_sender_t is deprecated. Please use " + "exec::asio::thread_unsafe_use_sender_t instead.")]] = exec::asio::thread_unsafe_use_sender_t; + inline constexpr auto const & use_sender [[deprecated("asioexec::use_sender is deprecated. " "Please use exec::asio::use_sender " "instead.")]] = exec::asio::use_sender; + + inline constexpr auto const & thread_unsafe_use_sender [[deprecated("asioexec::thread_unsafe_use_" + "sender is deprecated. " + "Please use " + "exec::asio::thread_unsafe_" + "use_sender instead.")]] + = exec::asio::thread_unsafe_use_sender; } // namespace asioexec diff --git a/include/exec/asio/completion_token.hpp b/include/exec/asio/completion_token.hpp index 4fbead55f..c1dda4f47 100644 --- a/include/exec/asio/completion_token.hpp +++ b/include/exec/asio/completion_token.hpp @@ -154,10 +154,7 @@ namespace experimental::execution::asio ::STDEXEC::set_error_t(std::exception_ptr), ::STDEXEC::set_stopped_t()>; - template - class completion_handler; - - template + template struct operation_state_base { class frame_; @@ -172,7 +169,8 @@ namespace experimental::execution::asio Receiver r_; asio_impl::cancellation_signal signal_; - std::recursive_mutex m_; + STDEXEC_IMMOVABLE_NO_UNIQUE_ADDRESS + Mutex m_; frame_* frames_{nullptr}; std::exception_ptr ex_; bool abandoned_{false}; @@ -278,12 +276,13 @@ namespace experimental::execution::asio callback_; }; - template + template class completion_handler { - operation_state_base* self_; + using operation_state_type_ = operation_state_base; + operation_state_type_* self_; public: - explicit completion_handler(operation_state_base& self) noexcept + explicit completion_handler(operation_state_type_& self) noexcept : self_(&self) {} @@ -300,7 +299,7 @@ namespace experimental::execution::asio // When this goes out of scope it might send set stopped or set error, or // it might defer that to the executor frames above us on the call stack // (if any) - typename operation_state_base::frame_ const frame(*self_); + typename operation_state_type_::frame_ const frame(*self_); self_->abandoned_ = true; } } @@ -347,17 +346,21 @@ namespace experimental::execution::asio return self_->signal_.slot(); } - operation_state_base& state() const noexcept + operation_state_type_& state() const noexcept { STDEXEC_ASSERT(self_); return *self_; } }; - template - class operation_state : operation_state_base + template + class operation_state : operation_state_base { - using base_ = operation_state_base; + using base_ = operation_state_base; Initiation init_; Args args_; public: @@ -382,7 +385,7 @@ namespace experimental::execution::asio [&](auto&&... args) { ::STDEXEC::__invoke(static_cast(init_), - completion_handler(*this), + completion_handler(*this), static_cast(args)...); }, std::move(args_)); @@ -407,10 +410,13 @@ namespace experimental::execution::asio } }; - template + template class sender { using args_type_ = std::tuple...>; + template + using operation_state_type_ = + operation_state, Initiation, args_type_>; public: using sender_concept = ::STDEXEC::sender_tag; @@ -437,16 +443,12 @@ namespace experimental::execution::asio std::remove_cvref_t, ::STDEXEC::completion_signatures_of_t>> constexpr auto connect(Receiver&& receiver) const & noexcept( - std::is_nothrow_constructible_v< - operation_state, Initiation, args_type_>, - Receiver, - Initiation const &, - args_type_ const &>) + std::is_nothrow_constructible_v, + Receiver, + Initiation const &, + args_type_ const &>) { - return operation_state, Initiation, args_type_>( - static_cast(receiver), - init_, - args_); + return operation_state_type_(static_cast(receiver), init_, args_); } template @@ -454,27 +456,26 @@ namespace experimental::execution::asio std::remove_cvref_t, ::STDEXEC::completion_signatures_of_t>> constexpr auto connect(Receiver&& receiver) && noexcept( - std::is_nothrow_constructible_v< - operation_state, Initiation, args_type_>, - Receiver, - Initiation, - args_type_>) + std::is_nothrow_constructible_v, + Receiver, + Initiation, + args_type_>) { - return operation_state, Initiation, args_type_>( - static_cast(receiver), - static_cast(init_), - static_cast(args_)); + return operation_state_type_(static_cast(receiver), + static_cast(init_), + static_cast(args_)); } private: Initiation init_; args_type_ args_; }; - template + template class executor { - operation_state_base& self_; - Executor ex_; + using operation_state_type_ = operation_state_base; + operation_state_type_& self_; + Executor ex_; template constexpr auto wrap_(F f) const noexcept(std::is_nothrow_move_constructible_v) @@ -485,8 +486,7 @@ namespace experimental::execution::asio }; } public: - constexpr explicit executor(operation_state_base& self, - Executor const & ex) noexcept + constexpr explicit executor(operation_state_type_& self, Executor const & ex) noexcept : self_(self) , ex_(ex) {} @@ -507,7 +507,7 @@ namespace experimental::execution::asio constexpr decltype(auto) prefer(Args&&... args) const noexcept { auto const ex = asio_impl::prefer(ex_, static_cast(args)...); - return executor>(self_, ex); + return executor>(self_, ex); } template @@ -517,7 +517,7 @@ namespace experimental::execution::asio constexpr decltype(auto) require(Args&&... args) const noexcept { auto const ex = asio_impl::require(ex_, static_cast(args)...); - return executor>(self_, ex); + return executor>(self_, ex); } template @@ -573,17 +573,32 @@ namespace experimental::execution::asio bool operator!=(executor const & rhs) const = default; }; + template + struct token + { + static constexpr auto as_default_on = asio::as_default_on; + template + using as_default_on_t = asio::as_default_on_t; + }; + + struct null_basic_lockable + { + constexpr void lock() noexcept {} + + constexpr void unlock() noexcept {} + }; + } // namespace detail::completion_token - struct completion_token_t - { - static constexpr auto as_default_on = asio::as_default_on; - template - using as_default_on_t = asio::as_default_on_t; - }; + using completion_token_t = detail::completion_token::token; inline completion_token_t const completion_token{}; + using thread_unsafe_completion_token_t = + detail::completion_token::token; + + inline thread_unsafe_completion_token_t const thread_unsafe_completion_token{}; + } // namespace experimental::execution::asio namespace exec = experimental::execution; @@ -591,48 +606,53 @@ namespace exec = experimental::execution; namespace ASIOEXEC_ASIO_NAMESPACE { - template - struct async_result<::exec::asio::completion_token_t, Signatures...> + template + struct async_result<::exec::asio::detail::completion_token::token, Signatures...> { template requires(std::is_constructible_v, Args> && ...) - static constexpr auto - initiate(Initiation&& i, ::exec::asio::completion_token_t const &, Args&&... args) + static constexpr auto initiate(Initiation&& i, + ::exec::asio::detail::completion_token::token const &, + Args&&... args) { return ::exec::asio::detail::completion_token::sender< + Mutex, ::exec::asio::detail::completion_token::completion_signatures, std::remove_cvref_t, Args...>(static_cast(i), static_cast(args)...); } }; - template + template struct associated_executor< - ::exec::asio::detail::completion_token::completion_handler, + ::exec::asio::detail::completion_token::completion_handler, Executor> { - using type = ::exec::asio::detail::completion_token::executor; + using type = + ::exec::asio::detail::completion_token::executor; - static type - get(::exec::asio::detail::completion_token::completion_handler const & h, - Executor const & ex) noexcept + static type get(::exec::asio::detail::completion_token::completion_handler const & h, + Executor const & ex) noexcept { return type(h.state(), ex); } }; - template + template requires ::STDEXEC::__callable<::STDEXEC::get_allocator_t, ::STDEXEC::env_of_t> struct associated_allocator< - ::exec::asio::detail::completion_token::completion_handler, + ::exec::asio::detail::completion_token::completion_handler, Allocator> { using type = std::remove_cvref_t())))>; - static type - get(::exec::asio::detail::completion_token::completion_handler const & h, - ::STDEXEC::__ignore = {}) noexcept + static type get(::exec::asio::detail::completion_token::completion_handler const & h, + ::STDEXEC::__ignore = {}) noexcept { return ::STDEXEC::get_allocator(::STDEXEC::get_env(h.state().r_)); } diff --git a/include/exec/asio/use_sender.hpp b/include/exec/asio/use_sender.hpp index 760b7b8d4..bb1ff8023 100644 --- a/include/exec/asio/use_sender.hpp +++ b/include/exec/asio/use_sender.hpp @@ -33,6 +33,7 @@ #include #include +#include #include #include #include @@ -215,17 +216,25 @@ namespace experimental::execution::asio template explicit sender(Sender) -> sender; + template + struct token + { + static constexpr auto as_default_on = asio::as_default_on; + template + using as_default_on_t = asio::as_default_on_t; + }; + } // namespace detail::use_sender - struct use_sender_t - { - static constexpr auto as_default_on = asio::as_default_on; - template - using as_default_on_t = asio::as_default_on_t; - }; + using use_sender_t = detail::use_sender::token; inline use_sender_t const use_sender{}; + using thread_unsafe_use_sender_t = + detail::use_sender::token; + + inline thread_unsafe_use_sender_t const thread_unsafe_use_sender{}; + } // namespace experimental::execution::asio namespace exec = experimental::execution; @@ -233,18 +242,18 @@ namespace exec = experimental::execution; namespace ASIOEXEC_ASIO_NAMESPACE { - template - struct async_result<::exec::asio::use_sender_t, Signatures...> + template + struct async_result<::exec::asio::detail::use_sender::token, Signatures...> { template requires(std::is_constructible_v, Args> && ...) static constexpr auto - initiate(Initiation&& i, ::exec::asio::use_sender_t const &, Args&&... args) + initiate(Initiation&& i, ::exec::asio::detail::use_sender::token const &, Args&&... args) { return ::exec::asio::detail::use_sender::sender( - async_result<::exec::asio::completion_token_t, Signatures...>::initiate( + async_result<::exec::asio::detail::completion_token::token, Signatures...>::initiate( static_cast(i), - ::exec::asio::completion_token, + ::exec::asio::detail::completion_token::token{}, static_cast(args)...)); } }; diff --git a/test/exec/asio/test_completion_token.cpp b/test/exec/asio/test_completion_token.cpp index 4cbcf63e6..b96bd1d2b 100644 --- a/test/exec/asio/test_completion_token.cpp +++ b/test/exec/asio/test_completion_token.cpp @@ -166,7 +166,7 @@ namespace asio_impl::io_context ctx; asio_impl::system_timer t(ctx); t.expires_after(std::chrono::years(1)); - auto sender = t.async_wait(completion_token); + auto sender = t.async_wait(thread_unsafe_completion_token); static_assert(set_equivalent>, completion_signatures); static_assert(::STDEXEC::sender_of); static_assert(