86.79% Lines (92/106)
100.00% Functions (13/13)
| TLA | Baseline | Branch | ||||||
|---|---|---|---|---|---|---|---|---|
| Line | Hits | Code | Line | Hits | Code | |||
| 1 | // | 1 | // | |||||
| 2 | // Copyright (c) 2026 Steve Gerbino | 2 | // Copyright (c) 2026 Steve Gerbino | |||||
| 3 | // Copyright (c) 2026 Michael Vandeberg | 3 | // Copyright (c) 2026 Michael Vandeberg | |||||
| 4 | // | 4 | // | |||||
| 5 | // Distributed under the Boost Software License, Version 1.0. (See accompanying | 5 | // Distributed under the Boost Software License, Version 1.0. (See accompanying | |||||
| 6 | // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) | 6 | // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) | |||||
| 7 | // | 7 | // | |||||
| 8 | // Official repository: https://github.com/cppalliance/capy | 8 | // Official repository: https://github.com/cppalliance/capy | |||||
| 9 | // | 9 | // | |||||
| 10 | 10 | |||||||
| 11 | #ifndef BOOST_CAPY_EX_ASYNC_WAKER_HPP | 11 | #ifndef BOOST_CAPY_EX_ASYNC_WAKER_HPP | |||||
| 12 | #define BOOST_CAPY_EX_ASYNC_WAKER_HPP | 12 | #define BOOST_CAPY_EX_ASYNC_WAKER_HPP | |||||
| 13 | 13 | |||||||
| 14 | #include <boost/capy/detail/config.hpp> | 14 | #include <boost/capy/detail/config.hpp> | |||||
| 15 | #include <boost/capy/continuation.hpp> | 15 | #include <boost/capy/continuation.hpp> | |||||
| 16 | #include <boost/capy/error.hpp> | 16 | #include <boost/capy/error.hpp> | |||||
| 17 | #include <boost/capy/ex/executor_ref.hpp> | 17 | #include <boost/capy/ex/executor_ref.hpp> | |||||
| 18 | #include <boost/capy/ex/io_env.hpp> | 18 | #include <boost/capy/ex/io_env.hpp> | |||||
| 19 | #include <boost/capy/io_result.hpp> | 19 | #include <boost/capy/io_result.hpp> | |||||
| 20 | 20 | |||||||
| 21 | #include <atomic> | 21 | #include <atomic> | |||||
| 22 | #include <coroutine> | 22 | #include <coroutine> | |||||
| 23 | #include <new> | 23 | #include <new> | |||||
| 24 | #include <stop_token> | 24 | #include <stop_token> | |||||
| 25 | + | #include <thread> | ||||||
| 25 | #include <utility> | 26 | #include <utility> | |||||
| 26 | 27 | |||||||
| 27 | /* async_waker implementation notes | 28 | /* async_waker implementation notes | |||||
| 28 | =================================== | 29 | =================================== | |||||
| 29 | 30 | |||||||
| 30 | wake() must be callable from foreign threads (that is the whole | 31 | wake() must be callable from foreign threads (that is the whole | |||||
| 31 | point: the user's thread provides the timing). A waiter-side | 32 | point: the user's thread provides the timing). A waiter-side | |||||
| 32 | claimed_ flag is not enough there -- the | 33 | claimed_ flag is not enough there -- the | |||||
| 33 | waker has to dereference the waiter, and nothing would pin the | 34 | waker has to dereference the waiter, and nothing would pin the | |||||
| 34 | waiter's frame between reading the pointer and claiming it. | 35 | waiter's frame between reading the pointer and claiming it. | |||||
| 35 | 36 | |||||||
| 36 | - | So the three-state st_ atomic is the single arbiter: | 37 | + | So the st_ atomic is the single arbiter: | |||
| 37 | 38 | |||||||
| 38 | - | empty --arm--> armed --wake/cancel CAS--> empty | 39 | + | empty --arm--> arming --publish--> armed --wake/cancel CAS--> empty | |||
| 40 | + | arming --stop already requested--> empty | ||||||
| 39 | empty --wake--> token --wait consumes--> empty | 41 | empty --wake--> token --wait consumes--> empty | |||||
| 40 | 42 | |||||||
| 41 | Whoever wins the armed->empty CAS owns the resume and may | 43 | Whoever wins the armed->empty CAS owns the resume and may | |||||
| 42 | dereference waiter_: the frame cannot die underneath the | 44 | dereference waiter_: the frame cannot die underneath the | |||||
| 43 | winner because the coroutine only resumes when the winner | 45 | winner because the coroutine only resumes when the winner | |||||
| 44 | posts it. The loser never touches the waiter. When the stop | 46 | posts it. The loser never touches the waiter. When the stop | |||||
| 45 | callback wins, a concurrent wake retries, finds empty, and | 47 | callback wins, a concurrent wake retries, finds empty, and | |||||
| 46 | latches a token -- a racing wakeup is deferred, never lost. | 48 | latches a token -- a racing wakeup is deferred, never lost. | |||||
| 47 | 49 | |||||||
| 48 | - | Serialized resumption is required: await_suspend keeps | 50 | + | The stop callback is registered before the waiter enters the | |||
| 49 | - | writing after the publishing armed-CAS (the stop_cb | 51 | + | arming state, and a wake() winner destroys it before posting. | |||
| 50 | - | placement-new and active_ = true), so a wake/cancel winner | 52 | + | While arming, nothing can claim the waiter: wake(), the stop | |||
| 51 | - | can post the continuation while that tail is still running. | 53 | + | callback and the destructor wait the few instructions it takes. | |||
| 52 | - | The posted resume must be ordered after await_suspend's | 54 | + | So the waiter settles a stop requested during registration | |||
| 53 | - | return, which holds on a single-threaded executor (the one | 55 | + | itself, its last touch of the waker is the publishing store, | |||
| 54 | - | thread is still inside await_suspend) and on a strand (the | 56 | + | and the waker can die right after the wake() that claimed it. | |||
| 55 | - | resume is a later turn, synchronized with the current one). | 57 | + | ||||
| 56 | - | A raw multi-threaded executor lets another worker run | 58 | + | Serialized resumption is still required: the executor must not | |||
| 57 | - | await_resume against those in-flight writes. async_event and | 59 | + | run the waiter's continuations concurrently, since a winner can | |||
| 58 | - | async_mutex make the same assumption; it is stated explicitly | 60 | + | post the waiter before its await_suspend has returned. async_event | |||
| 61 | + | and async_mutex make the same assumption; it is stated explicitly | ||||||
| 59 | here because wake() invites foreign threads into the picture. | 62 | here because wake() invites foreign threads into the picture. | |||||
| 60 | */ | 63 | */ | |||||
| 61 | 64 | |||||||
| 62 | namespace boost { | 65 | namespace boost { | |||||
| 63 | namespace capy { | 66 | namespace capy { | |||||
| 64 | 67 | |||||||
| 65 | /** A single-slot waker that hands one wakeup to a waiting coroutine. | 68 | /** A single-slot waker that hands one wakeup to a waiting coroutine. | |||||
| 66 | 69 | |||||||
| 67 | This is the escape hatch for timing and other external events: | 70 | This is the escape hatch for timing and other external events: | |||||
| 68 | the user provides the thread and the clock, capy provides the | 71 | the user provides the thread and the clock, capy provides the | |||||
| 69 | suspension point. One coroutine suspends in `wait()`; any | 72 | suspension point. One coroutine suspends in `wait()`; any | |||||
| 70 | thread wakes it with `wake()`. | 73 | thread wakes it with `wake()`. | |||||
| 71 | 74 | |||||||
| 72 | A wakeup with no waiter present is latched as a single pending | 75 | A wakeup with no waiter present is latched as a single pending | |||||
| 73 | token, and the next `wait()` consumes it immediately. This | 76 | token, and the next `wait()` consumes it immediately. This | |||||
| 74 | makes the wake-before-wait race benign without any lock | 77 | makes the wake-before-wait race benign without any lock | |||||
| 75 | protocol. Multiple wakes collapse into one token. | 78 | protocol. Multiple wakes collapse into one token. | |||||
| 76 | 79 | |||||||
| 77 | @par Cancellation | 80 | @par Cancellation | |||||
| 78 | 81 | |||||||
| 79 | If the environment's stop token is triggered while suspended, | 82 | If the environment's stop token is triggered while suspended, | |||||
| 80 | the wait completes with `error::canceled`. A wake that loses | 83 | the wait completes with `error::canceled`. A wake that loses | |||||
| 81 | the race against cancellation is latched for the next `wait()` | 84 | the race against cancellation is latched for the next `wait()` | |||||
| 82 | rather than dropped. | 85 | rather than dropped. | |||||
| 83 | 86 | |||||||
| 84 | @par Zero Allocation | 87 | @par Zero Allocation | |||||
| 85 | 88 | |||||||
| 86 | No heap allocation occurs for wait or wake operations. | 89 | No heap allocation occurs for wait or wake operations. | |||||
| 87 | 90 | |||||||
| 88 | @par Thread Safety | 91 | @par Thread Safety | |||||
| 89 | 92 | |||||||
| 90 | Distinct objects: Safe.@n | 93 | Distinct objects: Safe.@n | |||||
| 91 | Shared objects: `wake()` may be called from any thread. | 94 | Shared objects: `wake()` may be called from any thread. | |||||
| 92 | `wait()` must only be awaited by one coroutine at a time. The | 95 | `wait()` must only be awaited by one coroutine at a time. The | |||||
| 93 | executor must never run the coroutine's continuations | 96 | executor must never run the coroutine's continuations | |||||
| 94 | concurrently: use a single-threaded executor, or a strand over | 97 | concurrently: use a single-threaded executor, or a strand over | |||||
| 95 | a multi-threaded one. That is the same threading model as | 98 | a multi-threaded one. That is the same threading model as | |||||
| 96 | `async_event` and `async_mutex`. Awaiting `wait()` directly | 99 | `async_event` and `async_mutex`. Awaiting `wait()` directly | |||||
| 97 | on a multi-threaded executor is undefined. | 100 | on a multi-threaded executor is undefined. | |||||
| 98 | 101 | |||||||
| 99 | This type is non-copyable and non-movable because a suspended | 102 | This type is non-copyable and non-movable because a suspended | |||||
| 100 | waiter holds a pointer into the object. | 103 | waiter holds a pointer into the object. | |||||
| 101 | 104 | |||||||
| 102 | @par Example | 105 | @par Example | |||||
| 103 | @par !example example | 106 | @par !example example | |||||
| 104 | 107 | |||||||
| 105 | */ | 108 | */ | |||||
| 106 | class async_waker | 109 | class async_waker | |||||
| 107 | { | 110 | { | |||||
| 108 | public: | 111 | public: | |||||
| 109 | class wait_awaiter; | 112 | class wait_awaiter; | |||||
| 110 | 113 | |||||||
| 111 | private: | 114 | private: | |||||
| 112 | - | static constexpr int state_empty = 0; // no token, no waiter | 115 | + | static constexpr int state_empty = 0; // no token, no waiter | |||
| 113 | - | static constexpr int state_token = 1; // latched wakeup | 116 | + | static constexpr int state_token = 1; // latched wakeup | |||
| 114 | - | static constexpr int state_armed = 2; // waiter suspended | 117 | + | static constexpr int state_armed = 2; // waiter suspended | |||
| 118 | + | static constexpr int state_arming = 3; // waiter publishing itself | ||||||
| 115 | 119 | |||||||
| 116 | std::atomic<int> st_{state_empty}; | 120 | std::atomic<int> st_{state_empty}; | |||||
| 117 | wait_awaiter* waiter_ = nullptr; | 121 | wait_awaiter* waiter_ = nullptr; | |||||
| 118 | 122 | |||||||
| 123 | + | // Take the armed waiter, waiting out one still publishing itself. | ||||||
| HITGNC | 124 | + | 2427 | bool claim_armed() noexcept | ||||
| 125 | + | { | ||||||
| HITGNC | 126 | + | 2427 | int expected = state_armed; | ||||
| 127 | + | for(;;) | ||||||
| 128 | + | { | ||||||
| HITGNC | 129 | + | 4854 | if(st_.compare_exchange_weak( | ||||
| 130 | + | expected, state_empty, | ||||||
| 131 | + | std::memory_order_acq_rel, | ||||||
| 132 | + | std::memory_order_acquire)) | ||||||
| HITGNC | 133 | + | 2011 | return true; | ||||
| HITGNC | 134 | + | 416 | if(expected == state_armed) | ||||
| 135 | + | continue; // LCOV_EXCL_LINE spurious weak CAS failure | ||||||
| HITGNC | 136 | + | 416 | if(expected != state_arming) | ||||
| HITGNC | 137 | + | 416 | return false; | ||||
| 138 | + | std::this_thread::yield(); // LCOV_EXCL_LINE race with a waiter still publishing itself | ||||||
| 139 | + | expected = state_armed; // LCOV_EXCL_LINE race with a waiter still publishing itself | ||||||
| 140 | + | } | ||||||
| 141 | + | } | ||||||
| 142 | + | |||||||
| 119 | public: | 143 | public: | |||||
| 120 | /** Suspends the caller until `wake()` runs, or resumes it with `error::canceled` on a stop request. | 144 | /** Suspends the caller until `wake()` runs, or resumes it with `error::canceled` on a stop request. | |||||
| 121 | */ | 145 | */ | |||||
| 122 | class wait_awaiter | 146 | class wait_awaiter | |||||
| 123 | { | 147 | { | |||||
| 124 | friend class async_waker; | 148 | friend class async_waker; | |||||
| 125 | 149 | |||||||
| 126 | async_waker* waker_; | 150 | async_waker* waker_; | |||||
| 127 | continuation cont_; | 151 | continuation cont_; | |||||
| 128 | executor_ref ex_; | 152 | executor_ref ex_; | |||||
| 129 | 153 | |||||||
| 130 | // Declared before stop_cb_buf_: the callback accesses | 154 | // Declared before stop_cb_buf_: the callback accesses | |||||
| 131 | // these members, so they must still be alive if the | 155 | // these members, so they must still be alive if the | |||||
| 132 | // stop_cb_ destructor blocks. | 156 | // stop_cb_ destructor blocks. | |||||
| 133 | bool canceled_ = false; | 157 | bool canceled_ = false; | |||||
| 134 | bool active_ = false; | 158 | bool active_ = false; | |||||
| 135 | bool published_ = false; | 159 | bool published_ = false; | |||||
| 136 | 160 | |||||||
| 161 | + | // Set by whoever wins the armed->empty transition, before it | ||||||
| 162 | + | // posts. A claimed waiter belongs to the executor's queue and | ||||||
| 163 | + | // may be discarded after the waker is gone, so its destructor | ||||||
| 164 | + | // must not touch the waker. | ||||||
| 165 | + | std::atomic<bool> claimed_{false}; | ||||||
| 166 | + | |||||||
| 137 | struct cancel_fn | 167 | struct cancel_fn | |||||
| 138 | { | 168 | { | |||||
| 139 | wait_awaiter* self_; | 169 | wait_awaiter* self_; | |||||
| 140 | 170 | |||||||
| HITCBC | 141 | 20 | void operator()() const noexcept | 171 | 2009 | void operator()() const noexcept | ||
| 142 | { | 172 | { | |||||
| HITCBC | 143 | - | 20 | int expected = state_armed; | 173 | + | 2009 | if(self_->waker_->claim_armed()) |
| DCB | 144 | - | 40 | if(self_->waker_->st_.compare_exchange_strong( | ||||
| 145 | - | expected, state_empty, | ||||||
| 146 | - | std::memory_order_acq_rel, | ||||||
| 147 | - | std::memory_order_acquire)) | ||||||
| 148 | { | 174 | { | |||||
| HITCBC | 149 | 17 | self_->canceled_ = true; | 175 | 2009 | self_->canceled_ = true; | ||
| HITGNC | 176 | + | 2009 | self_->claimed_.store( | ||||
| 177 | + | true, std::memory_order_release); | ||||||
| HITCBC | 150 | 17 | self_->ex_.post(self_->cont_); | 178 | 2009 | self_->ex_.post(self_->cont_); | ||
| 151 | } | 179 | } | |||||
| HITCBC | 152 | 20 | } | 180 | 2009 | } | ||
| 153 | }; | 181 | }; | |||||
| 154 | 182 | |||||||
| 155 | using stop_cb_t = std::stop_callback<cancel_fn>; | 183 | using stop_cb_t = std::stop_callback<cancel_fn>; | |||||
| 156 | 184 | |||||||
| 157 | // Aligned storage for stop_cb_t. Declared last: its | 185 | // Aligned storage for stop_cb_t. Declared last: its | |||||
| 158 | // destructor may block while the callback accesses the | 186 | // destructor may block while the callback accesses the | |||||
| 159 | // members above. | 187 | // members above. | |||||
| 160 | BOOST_CAPY_MSVC_WARNING_PUSH | 188 | BOOST_CAPY_MSVC_WARNING_PUSH | |||||
| 161 | BOOST_CAPY_MSVC_WARNING_DISABLE(4324) | 189 | BOOST_CAPY_MSVC_WARNING_DISABLE(4324) | |||||
| 162 | alignas(stop_cb_t) | 190 | alignas(stop_cb_t) | |||||
| 163 | unsigned char stop_cb_buf_[sizeof(stop_cb_t)]; | 191 | unsigned char stop_cb_buf_[sizeof(stop_cb_t)]; | |||||
| 164 | BOOST_CAPY_MSVC_WARNING_POP | 192 | BOOST_CAPY_MSVC_WARNING_POP | |||||
| 165 | 193 | |||||||
| HITCBC | 166 | 49 | stop_cb_t& stop_cb_() noexcept | 194 | 2325 | stop_cb_t& stop_cb_() noexcept | ||
| 167 | { | 195 | { | |||||
| HITCBC | 168 | 49 | return *reinterpret_cast<stop_cb_t*>(stop_cb_buf_); | 196 | 2325 | return *reinterpret_cast<stop_cb_t*>(stop_cb_buf_); | ||
| 169 | } | 197 | } | |||||
| 170 | 198 | |||||||
| 171 | public: | 199 | public: | |||||
| 172 | /** Destroy the awaiter, leaving the waker unable to reach it. | 200 | /** Destroy the awaiter, leaving the waker unable to reach it. | |||||
| 173 | 201 | |||||||
| 174 | Destroys the stop callback if one is registered. If the awaiter | 202 | Destroys the stop callback if one is registered. If the awaiter | |||||
| 175 | is still armed, it also returns the waker's slot to the empty | 203 | is still armed, it also returns the waker's slot to the empty | |||||
| 176 | state, so a later `wake()` cannot dereference a destroyed | 204 | state, so a later `wake()` cannot dereference a destroyed | |||||
| 177 | awaiter. That case means the frame is being torn down without | 205 | awaiter. That case means the frame is being torn down without | |||||
| 178 | ever being resumed; a wake arriving afterward latches a token | 206 | ever being resumed; a wake arriving afterward latches a token | |||||
| 179 | instead. | 207 | instead. | |||||
| 180 | */ | 208 | */ | |||||
| HITCBC | 181 | 294 | ~wait_awaiter() | 209 | 4918 | ~wait_awaiter() | ||
| 182 | { | 210 | { | |||||
| HITCBC | 183 | 294 | if(active_) | 211 | 4918 | if(active_) | ||
| HITCBC | 184 | 1 | stop_cb_().~stop_cb_t(); | 212 | 3 | stop_cb_().~stop_cb_t(); | ||
| HITCBC | 185 | - | 294 | if(published_) | 213 | + | 4923 | if(published_ && |
| HITGNC | 214 | + | 5 | !claimed_.load(std::memory_order_acquire)) | ||||
| 186 | { | 215 | { | |||||
| 187 | // Destroyed while still armed (frame torn down | 216 | // Destroyed while still armed (frame torn down | |||||
| 188 | // without resuming): deregister so a later | 217 | // without resuming): deregister so a later | |||||
| 189 | // wake cannot touch the dead frame. | 218 | // wake cannot touch the dead frame. | |||||
| HITCBC | 190 | 1 | int expected = state_armed; | 219 | 1 | int expected = state_armed; | ||
| HITCBC | 191 | 1 | waker_->st_.compare_exchange_strong( | 220 | 1 | waker_->st_.compare_exchange_strong( | ||
| 192 | expected, state_empty, | 221 | expected, state_empty, | |||||
| 193 | std::memory_order_acq_rel, | 222 | std::memory_order_acq_rel, | |||||
| 194 | std::memory_order_acquire); | 223 | std::memory_order_acquire); | |||||
| 195 | } | 224 | } | |||||
| HITCBC | 196 | 294 | } | 225 | 4918 | } | ||
| 197 | 226 | |||||||
| 198 | /** Construct an awaiter for the given waker. | 227 | /** Construct an awaiter for the given waker. | |||||
| 199 | 228 | |||||||
| 200 | @param waker The waker to wait on. It must outlive the awaiter. | 229 | @param waker The waker to wait on. It must outlive the awaiter. | |||||
| 201 | */ | 230 | */ | |||||
| HITCBC | 202 | 147 | explicit wait_awaiter(async_waker* waker) noexcept | 231 | 2459 | explicit wait_awaiter(async_waker* waker) noexcept | ||
| HITCBC | 203 | 147 | : waker_(waker) | 232 | 2459 | : waker_(waker) | ||
| 204 | { | 233 | { | |||||
| HITCBC | 205 | 147 | } | 234 | 2459 | } | ||
| 206 | 235 | |||||||
| 207 | /** Construct by moving. | 236 | /** Construct by moving. | |||||
| 208 | 237 | |||||||
| 209 | The moved-from awaiter is left inert: its destructor no longer | 238 | The moved-from awaiter is left inert: its destructor no longer | |||||
| 210 | destroys the stop callback and no longer deregisters from the | 239 | destroys the stop callback and no longer deregisters from the | |||||
| 211 | waker. | 240 | waker. | |||||
| 212 | 241 | |||||||
| 213 | @param o The awaiter to move from. | 242 | @param o The awaiter to move from. | |||||
| 214 | */ | 243 | */ | |||||
| HITCBC | 215 | 147 | wait_awaiter(wait_awaiter&& o) noexcept | 244 | 2459 | wait_awaiter(wait_awaiter&& o) noexcept | ||
| HITCBC | 216 | 147 | : waker_(o.waker_) | 245 | 2459 | : waker_(o.waker_) | ||
| HITCBC | 217 | 147 | , cont_(o.cont_) | 246 | 2459 | , cont_(o.cont_) | ||
| HITCBC | 218 | 147 | , ex_(o.ex_) | 247 | 2459 | , ex_(o.ex_) | ||
| HITCBC | 219 | 147 | , canceled_(o.canceled_) | 248 | 2459 | , canceled_(o.canceled_) | ||
| HITCBC | 220 | 147 | , active_(std::exchange(o.active_, false)) | 249 | 2459 | , active_(std::exchange(o.active_, false)) | ||
| HITCBC | 221 | 147 | , published_(std::exchange(o.published_, false)) | 250 | 2459 | , published_(std::exchange(o.published_, false)) | ||
| HITGNC | 251 | + | 2459 | , claimed_(o.claimed_.load(std::memory_order_relaxed)) | ||||
| 222 | { | 252 | { | |||||
| HITCBC | 223 | 147 | } | 253 | 2459 | } | ||
| 224 | 254 | |||||||
| 225 | /** Copy construction is disabled; an armed waiter is registered | 255 | /** Copy construction is disabled; an armed waiter is registered | |||||
| 226 | with the waker by address. | 256 | with the waker by address. | |||||
| 227 | 257 | |||||||
| 228 | @param other The awaiter that would be copied. | 258 | @param other The awaiter that would be copied. | |||||
| 229 | */ | 259 | */ | |||||
| 230 | wait_awaiter(wait_awaiter const& other) = delete; | 260 | wait_awaiter(wait_awaiter const& other) = delete; | |||||
| 231 | 261 | |||||||
| 232 | /** Copy assignment is disabled; an armed waiter is registered | 262 | /** Copy assignment is disabled; an armed waiter is registered | |||||
| 233 | with the waker by address. | 263 | with the waker by address. | |||||
| 234 | 264 | |||||||
| 235 | @param other The awaiter that would be assigned from. | 265 | @param other The awaiter that would be assigned from. | |||||
| 236 | 266 | |||||||
| 237 | @return A reference to `*this`. | 267 | @return A reference to `*this`. | |||||
| 238 | */ | 268 | */ | |||||
| 239 | wait_awaiter& operator=(wait_awaiter const& other) = delete; | 269 | wait_awaiter& operator=(wait_awaiter const& other) = delete; | |||||
| 240 | 270 | |||||||
| 241 | /** Move assignment is disabled; an armed waiter is registered | 271 | /** Move assignment is disabled; an armed waiter is registered | |||||
| 242 | with the waker by address. | 272 | with the waker by address. | |||||
| 243 | 273 | |||||||
| 244 | @param other The awaiter that would be moved from. | 274 | @param other The awaiter that would be moved from. | |||||
| 245 | 275 | |||||||
| 246 | @return A reference to `*this`. | 276 | @return A reference to `*this`. | |||||
| 247 | */ | 277 | */ | |||||
| 248 | wait_awaiter& operator=(wait_awaiter&& other) = delete; | 278 | wait_awaiter& operator=(wait_awaiter&& other) = delete; | |||||
| 249 | 279 | |||||||
| 250 | /** Consume a latched token, completing synchronously. | 280 | /** Consume a latched token, completing synchronously. | |||||
| 251 | 281 | |||||||
| 252 | This is not a pure query: the check is a compare-exchange that | 282 | This is not a pure query: the check is a compare-exchange that | |||||
| 253 | takes the token. Calling it twice is not idempotent: the second | 283 | takes the token. Calling it twice is not idempotent: the second | |||||
| 254 | call reports `false`, because the first already consumed the | 284 | call reports `false`, because the first already consumed the | |||||
| 255 | wakeup. | 285 | wakeup. | |||||
| 256 | 286 | |||||||
| 257 | @return `true` if a pending wakeup token was latched and has now | 287 | @return `true` if a pending wakeup token was latched and has now | |||||
| 258 | been consumed, in which case the awaiting coroutine does not | 288 | been consumed, in which case the awaiting coroutine does not | |||||
| 259 | suspend; otherwise `false`. | 289 | suspend; otherwise `false`. | |||||
| 260 | */ | 290 | */ | |||||
| HITCBC | 261 | 147 | bool await_ready() noexcept | 291 | 2459 | bool await_ready() noexcept | ||
| 262 | { | 292 | { | |||||
| HITCBC | 263 | 147 | int expected = state_token; | 293 | 2459 | int expected = state_token; | ||
| HITCBC | 264 | 147 | return waker_->st_.compare_exchange_strong( | 294 | 2459 | return waker_->st_.compare_exchange_strong( | ||
| 265 | expected, state_empty, | 295 | expected, state_empty, | |||||
| 266 | std::memory_order_acq_rel, | 296 | std::memory_order_acq_rel, | |||||
| HITCBC | 267 | 147 | std::memory_order_acquire); | 297 | 2459 | std::memory_order_acquire); | ||
| 268 | } | 298 | } | |||||
| 269 | 299 | |||||||
| 270 | /** Arm the waker with the awaiting coroutine. | 300 | /** Arm the waker with the awaiting coroutine. | |||||
| 271 | 301 | |||||||
| 272 | This is the @ref IoAwaitable overload of `await_suspend`. | 302 | This is the @ref IoAwaitable overload of `await_suspend`. | |||||
| 273 | Unlike `async_event` and `async_mutex`, it has three outcomes, | 303 | Unlike `async_event` and `async_mutex`, it has three outcomes, | |||||
| 274 | because a `wake()` from another thread can land in the window | 304 | because a `wake()` from another thread can land in the window | |||||
| 275 | between `await_ready` and this call. | 305 | between `await_ready` and this call. | |||||
| 276 | 306 | |||||||
| 277 | @li A stop request is already pending on `env->stop_token`: the | 307 | @li A stop request is already pending on `env->stop_token`: the | |||||
| 278 | awaiter records the cancellation and does not arm. | 308 | awaiter records the cancellation and does not arm. | |||||
| 279 | 309 | |||||||
| 280 | @li The waker's slot is no longer empty. Under the single-waiter | 310 | @li The waker's slot is no longer empty. Under the single-waiter | |||||
| 281 | precondition that means a wakeup was latched after | 311 | precondition that means a wakeup was latched after | |||||
| 282 | `await_ready` looked, so the token is consumed here instead | 312 | `await_ready` looked, so the token is consumed here instead | |||||
| 283 | and the wait succeeds. | 313 | and the wait succeeds. | |||||
| 284 | 314 | |||||||
| 285 | - | @li Otherwise the slot moves to the armed state, publishing this | 315 | + | @li Otherwise a stop callback is registered on | |||
| 286 | - | awaiter to the waker, and a stop callback is registered on | 316 | + | `env->stop_token` and the slot moves to the armed state, | |||
| 287 | - | `env->stop_token`. Whichever of `wake()` and that callback | 317 | + | publishing this awaiter to the waker. A stop requested | |||
| 288 | - | wins the armed-to-empty transition posts `h` through | 318 | + | while the callback registered completes the wait canceled | |||
| 289 | - | `env->executor`. The loser does nothing, and a losing | 319 | + | instead. Whichever of `wake()` and that callback wins the | |||
| 290 | - | `wake()` re-latches its token for the next `wait()`. | 320 | + | armed-to-empty transition posts `h` through | |||
| 321 | + | `env->executor`; a winning `wake()` first deregisters the | ||||||
| 322 | + | callback. The loser does nothing, and a losing `wake()` | ||||||
| 323 | + | re-latches its token for the next `wait()`. | ||||||
| 291 | 324 | |||||||
| 292 | @param h The awaiting coroutine, resumed when the waker fires | 325 | @param h The awaiting coroutine, resumed when the waker fires | |||||
| 293 | or the wait is canceled. | 326 | or the wait is canceled. | |||||
| 294 | 327 | |||||||
| 295 | @param env The execution environment. Its executor posts the | 328 | @param env The execution environment. Its executor posts the | |||||
| 296 | resumption and its stop token is watched for the duration of | 329 | resumption and its stop token is watched for the duration of | |||||
| 297 | the wait. It must outlive the wait. | 330 | the wait. It must outlive the wait. | |||||
| 298 | 331 | |||||||
| 299 | @return `h` in the first two cases, which resumes the awaiting | 332 | @return `h` in the first two cases, which resumes the awaiting | |||||
| 300 | coroutine immediately; otherwise `std::noop_coroutine()`, which | 333 | coroutine immediately; otherwise `std::noop_coroutine()`, which | |||||
| 301 | leaves the coroutine suspended and returns control to the | 334 | leaves the coroutine suspended and returns control to the | |||||
| 302 | resumer. | 335 | resumer. | |||||
| 303 | */ | 336 | */ | |||||
| 304 | std::coroutine_handle<> | 337 | std::coroutine_handle<> | |||||
| HITCBC | 305 | 72 | await_suspend( | 338 | 2361 | await_suspend( | ||
| 306 | std::coroutine_handle<> h, | 339 | std::coroutine_handle<> h, | |||||
| 307 | io_env const* env) noexcept | 340 | io_env const* env) noexcept | |||||
| 308 | { | 341 | { | |||||
| HITCBC | 309 | 72 | if(env->stop_token.stop_requested()) | 342 | 2361 | if(env->stop_token.stop_requested()) | ||
| 310 | { | 343 | { | |||||
| HITCBC | 311 | 23 | canceled_ = true; | 344 | 36 | canceled_ = true; | ||
| HITCBC | 312 | 23 | return h; | 345 | 36 | return h; | ||
| 313 | } | 346 | } | |||||
| HITCBC | 314 | 49 | cont_.h = h; | 347 | 2325 | cont_.h = h; | ||
| HITGNC | 348 | + | 2325 | cont_.owner = env->owner; | ||||
| HITCBC | 315 | 49 | ex_ = env->executor; | 349 | 2325 | ex_ = env->executor; | ||
| HITCBC | 316 | 49 | waker_->waiter_ = this; | 350 | 2325 | waker_->waiter_ = this; | ||
| 317 | 351 | |||||||
| 352 | + | // Registered before publishing, so the wake() that claims | ||||||
| 353 | + | // this waiter can deregister it before posting. | ||||||
| HITGNC | 354 | + | 6975 | ::new(stop_cb_buf_) stop_cb_t( | ||||
| HITGNC | 355 | + | 2325 | env->stop_token, cancel_fn{this}); | ||||
| HITGNC | 356 | + | 2325 | active_ = true; | ||||
| HITGNC | 357 | + | 2325 | published_ = true; | ||||
| 358 | + | |||||||
| HITCBC | 318 | 49 | int expected = state_empty; | 359 | 2325 | int expected = state_empty; | ||
| HITCBC | 319 | 98 | if(!waker_->st_.compare_exchange_strong( | 360 | 4650 | if(!waker_->st_.compare_exchange_strong( | ||
| 320 | - | expected, state_armed, | 361 | + | expected, state_arming, | |||
| 321 | std::memory_order_acq_rel, | 362 | std::memory_order_acq_rel, | |||||
| 322 | std::memory_order_acquire)) | 363 | std::memory_order_acquire)) | |||||
| 323 | { | 364 | { | |||||
| 324 | // Single-waiter precondition: a second concurrent | 365 | // Single-waiter precondition: a second concurrent | |||||
| 325 | // wait would find the slot armed. | 366 | // wait would find the slot armed. | |||||
| MISUBC | 326 | ✗ | BOOST_CAPY_ASSERT(expected == state_token); | 367 | ✗ | BOOST_CAPY_ASSERT(expected == state_token); | ||
| 327 | 368 | |||||||
| 328 | // A wake latched between await_ready and here; | 369 | // A wake latched between await_ready and here; | |||||
| 329 | // consume it and resume inline. | 370 | // consume it and resume inline. | |||||
| MISUNC | 371 | + | ✗ | published_ = false; | ||||
| MISUNC | 372 | + | ✗ | stop_cb_().~stop_cb_t(); | ||||
| MISUNC | 373 | + | ✗ | active_ = false; | ||||
| MISUBC | 330 | ✗ | waker_->st_.store( | 374 | ✗ | waker_->st_.store( | ||
| 331 | state_empty, std::memory_order_release); | 375 | state_empty, std::memory_order_release); | |||||
| MISUBC | 332 | ✗ | return h; | 376 | ✗ | return h; | ||
| 333 | - | published_ = true; | ||||||
| ECB | 334 | 49 | } | 377 | } | |||
| 335 | 378 | |||||||
| ECB | 336 | - | 147 | ::new(stop_cb_buf_) stop_cb_t( | 379 | + | // While arming nothing else can claim this waiter, so a stop | |
| ECB | 337 | - | 49 | env->stop_token, cancel_fn{this}); | 380 | + | // requested as the callback registered is settled here, and | |
| ECB | 338 | - | 49 | active_ = true; | 381 | + | // the waker is not touched again once the waiter is armed. | |
| HITGNC | 382 | + | 2325 | if(env->stop_token.stop_requested()) | ||||
| 383 | + | { | ||||||
| MISUNC | 384 | + | ✗ | published_ = false; | ||||
| MISUNC | 385 | + | ✗ | waker_->st_.store( | ||||
| 386 | + | state_empty, std::memory_order_release); | ||||||
| MISUNC | 387 | + | ✗ | canceled_ = true; | ||||
| MISUNC | 388 | + | ✗ | return h; | ||||
| 389 | + | } | ||||||
| HITGNC | 390 | + | 2325 | waker_->st_.store(state_armed, std::memory_order_release); | ||||
| HITCBC | 339 | 49 | return std::noop_coroutine(); | 391 | 2325 | return std::noop_coroutine(); | ||
| 340 | } | 392 | } | |||||
| 341 | 393 | |||||||
| 342 | /** Complete the wait and report the outcome. | 394 | /** Complete the wait and report the outcome. | |||||
| 343 | 395 | |||||||
| 344 | Destroys the stop callback if one is registered and clears the | 396 | Destroys the stop callback if one is registered and clears the | |||||
| 345 | armed bookkeeping, so the destructor does not deregister a slot | 397 | armed bookkeeping, so the destructor does not deregister a slot | |||||
| 346 | the resumption already consumed. | 398 | the resumption already consumed. | |||||
| 347 | 399 | |||||||
| 348 | @return An empty `io_result<>` if the wait was woken, whether by | 400 | @return An empty `io_result<>` if the wait was woken, whether by | |||||
| 349 | `wake()` or by a token consumed inline. Otherwise one holding | 401 | `wake()` or by a token consumed inline. Otherwise one holding | |||||
| 350 | - | `error::canceled`, which means the stop token won the race. | 402 | + | `error::canceled`, which means the stop token won the race or | |||
| 403 | + | the waker was destroyed. | ||||||
| 351 | */ | 404 | */ | |||||
| HITCBC | 352 | 146 | [[nodiscard]] io_result<> await_resume() noexcept | 405 | 2454 | [[nodiscard]] io_result<> await_resume() noexcept | ||
| 353 | { | 406 | { | |||||
| HITCBC | 354 | 146 | if(active_) | 407 | 2454 | if(active_) | ||
| 355 | { | 408 | { | |||||
| HITCBC | 356 | 48 | stop_cb_().~stop_cb_t(); | 409 | 2007 | stop_cb_().~stop_cb_t(); | ||
| HITCBC | 357 | 48 | active_ = false; | 410 | 2007 | active_ = false; | ||
| 358 | } | 411 | } | |||||
| HITCBC | 359 | 146 | published_ = false; | 412 | 2454 | published_ = false; | ||
| HITCBC | 360 | 146 | if(canceled_) | 413 | 2454 | if(canceled_) | ||
| HITCBC | 361 | 39 | return {make_error_code(error::canceled)}; | 414 | 2045 | return {make_error_code(error::canceled)}; | ||
| HITCBC | 362 | 107 | return {{}}; | 415 | 409 | return {{}}; | ||
| 363 | } | 416 | } | |||||
| 364 | }; | 417 | }; | |||||
| 365 | 418 | |||||||
| 366 | /// Construct with no token latched. | 419 | /// Construct with no token latched. | |||||
| HITCBC | 367 | 1 | async_waker() = default; | 420 | 307 | async_waker() = default; | ||
| 368 | 421 | |||||||
| 422 | + | /** Destroy the waker, canceling a coroutine still waiting. | ||||||
| 423 | + | |||||||
| 424 | + | The waiter is posted to its executor to resume with | ||||||
| 425 | + | `error::canceled`, as if its stop token had been triggered. An | ||||||
| 426 | + | executor that can no longer run it abandons it instead, possibly | ||||||
| 427 | + | before this destructor returns. Either way the waiter no longer | ||||||
| 428 | + | refers to the waker. | ||||||
| 429 | + | |||||||
| 430 | + | @pre No `wake()` call is in progress or made afterward. | ||||||
| 431 | + | */ | ||||||
| HITGNC | 432 | + | 418 | ~async_waker() | ||||
| 433 | + | { | ||||||
| HITGNC | 434 | + | 418 | if(claim_armed()) | ||||
| 435 | + | { | ||||||
| HITGNC | 436 | + | 2 | auto* w = waiter_; | ||||
| 437 | + | // The stop callback reaches into the waker; deregistering | ||||||
| 438 | + | // it waits out one already running. | ||||||
| HITGNC | 439 | + | 2 | if(w->active_) | ||||
| 440 | + | { | ||||||
| 441 | + | using cb_t = wait_awaiter::stop_cb_t; | ||||||
| HITGNC | 442 | + | 2 | w->stop_cb_().~cb_t(); | ||||
| HITGNC | 443 | + | 2 | w->active_ = false; | ||||
| 444 | + | } | ||||||
| HITGNC | 445 | + | 2 | w->canceled_ = true; | ||||
| HITGNC | 446 | + | 2 | w->claimed_.store(true, std::memory_order_release); | ||||
| HITGNC | 447 | + | 2 | w->ex_.post(w->cont_); | ||||
| 448 | + | } | ||||||
| HITGNC | 449 | + | 418 | } | ||||
| 450 | + | |||||||
| 369 | /** Copy construction is disabled; an armed waiter points into the | 451 | /** Copy construction is disabled; an armed waiter points into the | |||||
| 370 | waker. | 452 | waker. | |||||
| 371 | 453 | |||||||
| 372 | @param other The waker that would be copied. | 454 | @param other The waker that would be copied. | |||||
| 373 | */ | 455 | */ | |||||
| 374 | async_waker(async_waker const& other) = delete; | 456 | async_waker(async_waker const& other) = delete; | |||||
| 375 | 457 | |||||||
| 376 | /** Copy assignment is disabled; an armed waiter points into the waker. | 458 | /** Copy assignment is disabled; an armed waiter points into the waker. | |||||
| 377 | 459 | |||||||
| 378 | @param other The waker that would be assigned from. | 460 | @param other The waker that would be assigned from. | |||||
| 379 | 461 | |||||||
| 380 | @return A reference to `*this`. | 462 | @return A reference to `*this`. | |||||
| 381 | */ | 463 | */ | |||||
| 382 | async_waker& operator=(async_waker const& other) = delete; | 464 | async_waker& operator=(async_waker const& other) = delete; | |||||
| 383 | 465 | |||||||
| 384 | /** Move construction is disabled; an armed waiter points into the | 466 | /** Move construction is disabled; an armed waiter points into the | |||||
| 385 | waker. | 467 | waker. | |||||
| 386 | 468 | |||||||
| 387 | @param other The waker that would be moved from. | 469 | @param other The waker that would be moved from. | |||||
| 388 | */ | 470 | */ | |||||
| 389 | async_waker(async_waker&& other) = delete; | 471 | async_waker(async_waker&& other) = delete; | |||||
| 390 | 472 | |||||||
| 391 | /** Move assignment is disabled; an armed waiter points into the waker. | 473 | /** Move assignment is disabled; an armed waiter points into the waker. | |||||
| 392 | 474 | |||||||
| 393 | @param other The waker that would be moved from. | 475 | @param other The waker that would be moved from. | |||||
| 394 | 476 | |||||||
| 395 | @return A reference to `*this`. | 477 | @return A reference to `*this`. | |||||
| 396 | */ | 478 | */ | |||||
| 397 | async_waker& operator=(async_waker&& other) = delete; | 479 | async_waker& operator=(async_waker&& other) = delete; | |||||
| 398 | 480 | |||||||
| 399 | /** Asynchronously wait until woken. | 481 | /** Asynchronously wait until woken. | |||||
| 400 | 482 | |||||||
| 401 | If a token is latched, completes immediately and consumes | 483 | If a token is latched, completes immediately and consumes | |||||
| 402 | it. Otherwise suspends until `wake()` or the stop token | 484 | it. Otherwise suspends until `wake()` or the stop token | |||||
| 403 | fires. | 485 | fires. | |||||
| 404 | 486 | |||||||
| 405 | @par Preconditions | 487 | @par Preconditions | |||||
| 406 | - | No other coroutine is currently waiting on this object. | 488 | + | No other coroutine is currently waiting on this object; a | |||
| 489 | + | coroutine woken or canceled but not yet resumed still is. | ||||||
| 407 | 490 | |||||||
| 408 | @return An awaitable that await-returns `io_result<>`; | 491 | @return An awaitable that await-returns `io_result<>`; | |||||
| 409 | empty on wakeup, `error::canceled` if the stop | 492 | empty on wakeup, `error::canceled` if the stop | |||||
| 410 | token wins. | 493 | token wins. | |||||
| 411 | */ | 494 | */ | |||||
| HITCBC | 412 | 147 | wait_awaiter wait() noexcept | 495 | 2459 | wait_awaiter wait() noexcept | ||
| 413 | { | 496 | { | |||||
| HITCBC | 414 | 147 | return wait_awaiter{this}; | 497 | 2459 | return wait_awaiter{this}; | ||
| 415 | } | 498 | } | |||||
| 416 | 499 | |||||||
| 417 | /** Wake the waiter, or latch the wakeup if none waits. | 500 | /** Wake the waiter, or latch the wakeup if none waits. | |||||
| 418 | 501 | |||||||
| 419 | Callable from any thread. The waiter's resumption is | 502 | Callable from any thread. The waiter's resumption is | |||||
| 420 | posted through its executor; this call never resumes a | 503 | posted through its executor; this call never resumes a | |||||
| 421 | coroutine inline. Multiple calls without an intervening | 504 | coroutine inline. Multiple calls without an intervening | |||||
| 422 | - | `wait()` collapse into a single token. | 505 | + | `wait()` collapse into a single token. It may wait briefly | |||
| 506 | + | for a waiter that is still suspending, or for a stop callback | ||||||
| 507 | + | of the waiter that is running on another thread. | ||||||
| 423 | */ | 508 | */ | |||||
| HITCBC | 424 | 109 | void wake() noexcept | 509 | 413 | void wake() noexcept | ||
| 425 | { | 510 | { | |||||
| 426 | for(;;) | 511 | for(;;) | |||||
| 427 | { | 512 | { | |||||
| HITCBC | 428 | 109 | int s = st_.load(std::memory_order_acquire); | 513 | 413 | int s = st_.load(std::memory_order_acquire); | ||
| HITCBC | 429 | 109 | if(s == state_token) | 514 | 413 | if(s == state_token) | ||
| HITCBC | 430 | 109 | return; | 515 | 413 | return; | ||
| HITCBC | 431 | 107 | if(s == state_empty) | 516 | 411 | if(s == state_empty) | ||
| 432 | { | 517 | { | |||||
| HITCBC | 433 | 150 | if(st_.compare_exchange_weak( | 518 | 196 | if(st_.compare_exchange_weak( | ||
| 434 | s, state_token, | 519 | s, state_token, | |||||
| 435 | std::memory_order_acq_rel, | 520 | std::memory_order_acq_rel, | |||||
| 436 | std::memory_order_acquire)) | 521 | std::memory_order_acquire)) | |||||
| HITCBC | 437 | 75 | return; | 522 | 98 | return; | ||
| MISUBC | 438 | ✗ | continue; | 523 | ✗ | continue; | ||
| 439 | } | 524 | } | |||||
| 525 | + | // The waiter is still publishing itself; it settles in a | ||||||
| 526 | + | // few instructions. | ||||||
| HITGNC | 527 | + | 313 | if(s == state_arming) | ||||
| 528 | + | { | ||||||
| MISUNC | 529 | + | ✗ | std::this_thread::yield(); | ||||
| MISUNC | 530 | + | ✗ | continue; | ||||
| 531 | + | } | ||||||
| 440 | // armed: winning this CAS claims the waiter, whose | 532 | // armed: winning this CAS claims the waiter, whose | |||||
| 441 | // frame is pinned until we post its resumption. | 533 | // frame is pinned until we post its resumption. | |||||
| HITCBC | 442 | 64 | if(st_.compare_exchange_weak( | 534 | 626 | if(st_.compare_exchange_weak( | ||
| 443 | s, state_empty, | 535 | s, state_empty, | |||||
| 444 | std::memory_order_acq_rel, | 536 | std::memory_order_acq_rel, | |||||
| 445 | std::memory_order_acquire)) | 537 | std::memory_order_acquire)) | |||||
| 446 | { | 538 | { | |||||
| HITCBC | 447 | 32 | auto* w = waiter_; | 539 | 313 | auto* w = waiter_; | ||
| 540 | + | // Nothing may reach this waker once the waiter is | ||||||
| 541 | + | // posted; this waits out a stop callback already | ||||||
| 542 | + | // running, which then finds the slot empty. | ||||||
| 543 | + | using cb_t = wait_awaiter::stop_cb_t; | ||||||
| HITGNC | 544 | + | 313 | w->stop_cb_().~cb_t(); | ||||
| HITGNC | 545 | + | 313 | w->active_ = false; | ||||
| HITGNC | 546 | + | 313 | w->claimed_.store(true, std::memory_order_release); | ||||
| HITCBC | 448 | 32 | w->ex_.post(w->cont_); | 547 | 313 | w->ex_.post(w->cont_); | ||
| HITCBC | 449 | 32 | return; | 548 | 313 | return; | ||
| 450 | } | 549 | } | |||||
| MISUBC | 451 | ✗ | } | 550 | ✗ | } | ||
| 452 | } | 551 | } | |||||
| 453 | }; | 552 | }; | |||||
| 454 | 553 | |||||||
| 455 | } // namespace capy | 554 | } // namespace capy | |||||
| 456 | } // namespace boost | 555 | } // namespace boost | |||||
| 457 | 556 | |||||||
| 458 | #endif | 557 | #endif | |||||