100.00% Lines (25/25)
100.00% Functions (7/7)
| 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 | // | 4 | // | |||||
| 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying | 5 | // Distributed under the Boost Software License, Version 1.0. (See accompanying | |||||
| 5 | // 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) | |||||
| 6 | // | 7 | // | |||||
| 7 | // Official repository: https://github.com/cppalliance/capy | 8 | // Official repository: https://github.com/cppalliance/capy | |||||
| 8 | // | 9 | // | |||||
| 9 | 10 | |||||||
| 10 | #ifndef BOOST_CAPY_EX_ABANDON_HPP | 11 | #ifndef BOOST_CAPY_EX_ABANDON_HPP | |||||
| 11 | #define BOOST_CAPY_EX_ABANDON_HPP | 12 | #define BOOST_CAPY_EX_ABANDON_HPP | |||||
| 12 | 13 | |||||||
| 13 | #include <boost/capy/detail/config.hpp> | 14 | #include <boost/capy/detail/config.hpp> | |||||
| 14 | #include <boost/capy/continuation.hpp> | 15 | #include <boost/capy/continuation.hpp> | |||||
| 15 | #include <boost/capy/ex/executor_ref.hpp> | 16 | #include <boost/capy/ex/executor_ref.hpp> | |||||
| 16 | 17 | |||||||
| 17 | #include <atomic> | 18 | #include <atomic> | |||||
| 18 | #include <coroutine> | 19 | #include <coroutine> | |||||
| 19 | 20 | |||||||
| 20 | namespace boost { | 21 | namespace boost { | |||||
| 21 | namespace capy { | 22 | namespace capy { | |||||
| 22 | 23 | |||||||
| 23 | /** The owner of one segment of a coroutine chain. | 24 | /** The owner of one segment of a coroutine chain. | |||||
| 24 | 25 | |||||||
| 25 | A segment is a run of coroutines started together: the task | 26 | A segment is a run of coroutines started together: the task | |||||
| 26 | `run_async` launches and everything it awaits, a task `run` starts | 27 | `run_async` launches and everything it awaits, a task `run` starts | |||||
| 27 | - | on another executor, or one branch of `when_all` or `when_any`. Its | 28 | + | on another executor, one branch of `when_all` or `when_any`, or a | |||
| 29 | + | @ref quitter awaited by a coroutine that is not a quitter. Its | ||||||
| 28 | owner is the coroutine that started it, suspended above the | 30 | owner is the coroutine that started it, suspended above the | |||||
| 29 | segment's top frame until that frame completes. Every coroutine in | 31 | segment's top frame until that frame completes. Every coroutine in | |||||
| 30 | the segment reaches the owner through its @ref await_context. | 32 | the segment reaches the owner through its @ref await_context. | |||||
| 31 | 33 | |||||||
| 32 | When the holder of a suspended coroutine in the segment gives it up | 34 | When the holder of a suspended coroutine in the segment gives it up | |||||
| 33 | with @ref abandon, the owner posts its own continuation to its | 35 | with @ref abandon, the owner posts its own continuation to its | |||||
| 34 | executor. There it unwinds the segment from the top: it destroys | 36 | executor. There it unwinds the segment from the top: it destroys | |||||
| 35 | - | the frame it awaits, which destroys the frames below it in turn, and | 37 | + | the frame it awaits, which destroys the frames below it in turn. | |||
| 36 | - | finishes without delivering a result. An executor that destroys the | 38 | + | After this teardown, the owner delivers no result. An executor that | |||
| 37 | - | posted continuation instead, as a closed one does, unwinds the | 39 | + | destroys the posted continuation instead, as a closed one does, | |||
| 38 | - | segment the same way. | 40 | + | unwinds the segment the same way. | |||
| 41 | + | |||||||
| 42 | + | The owner also records why the segment was abandoned. Teardown | ||||||
| 43 | + | calls @ref abandon. A stopped @ref quitter calls | ||||||
| 44 | + | @ref abandon_for_stop instead. Its owner unwinds the segment the | ||||||
| 45 | + | same way, then reports the stop to its parent. An owner that never | ||||||
| 46 | + | asks @ref stopped treats a stop as teardown: it delivers nothing. A | ||||||
| 47 | + | launcher written by a user must therefore check @ref stopped if it | ||||||
| 48 | + | runs quitters and has to report their stops. | ||||||
| 39 | 49 | |||||||
| 40 | @par Thread Safety | 50 | @par Thread Safety | |||||
| 41 | - | @ref abandon may be called from any thread. Only one holder can | 51 | + | @ref abandon and @ref abandon_for_stop may be called from any | |||
| 42 | - | abandon a segment, since it is suspended at one point; a repeated | 52 | + | thread. Only the first call takes effect, and decides the cause; a | |||
| 43 | - | call before the unwind runs does nothing. | 53 | + | later call of either before the unwind runs does nothing. | |||
| 44 | */ | 54 | */ | |||||
| 45 | class chain_owner | 55 | class chain_owner | |||||
| 46 | { | 56 | { | |||||
| 47 | - | std::atomic<bool> abandoned_{false}; | 57 | + | enum : unsigned char { not_abandoned, by_teardown, by_stop }; | |||
| 58 | + | |||||||
| 59 | + | std::atomic<unsigned char> cause_{not_abandoned}; | ||||||
| 48 | continuation self_; | 60 | continuation self_; | |||||
| 49 | executor_ref ex_; | 61 | executor_ref ex_; | |||||
| 50 | 62 | |||||||
| HITGNC | 63 | + | 126211 | void abandon_as(unsigned char cause) noexcept | ||||
| 64 | + | { | ||||||
| HITGNC | 65 | + | 126211 | unsigned char expected = not_abandoned; | ||||
| HITGNC | 66 | + | 252422 | if(!cause_.compare_exchange_strong( | ||||
| 67 | + | expected, cause, | ||||||
| 68 | + | std::memory_order_acq_rel, | ||||||
| 69 | + | std::memory_order_acquire)) | ||||||
| HITGNC | 70 | + | 10002 | return; | ||||
| HITGNC | 71 | + | 116209 | ex_.post(self_); | ||||
| 72 | + | } | ||||||
| 73 | + | |||||||
| 51 | public: | 74 | public: | |||||
| 52 | /** Bind the owner to its coroutine and executor. | 75 | /** Bind the owner to its coroutine and executor. | |||||
| 53 | 76 | |||||||
| 54 | Called by the owner before it starts the segment. | 77 | Called by the owner before it starts the segment. | |||||
| 55 | 78 | |||||||
| 56 | @param self The owner's coroutine, resumed or destroyed to | 79 | @param self The owner's coroutine, resumed or destroyed to | |||||
| 57 | unwind the segment. | 80 | unwind the segment. | |||||
| 58 | 81 | |||||||
| 59 | @param ex The executor the segment runs on. | 82 | @param ex The executor the segment runs on. | |||||
| 60 | */ | 83 | */ | |||||
| HITCBC | 61 | 106453 | void bind(std::coroutine_handle<> self, executor_ref ex) noexcept | 84 | 136552 | void bind(std::coroutine_handle<> self, executor_ref ex) noexcept | ||
| 62 | { | 85 | { | |||||
| HITCBC | 63 | 106453 | self_.h = self; | 86 | 136552 | self_.h = self; | ||
| HITCBC | 64 | 106453 | ex_ = ex; | 87 | 136552 | ex_ = ex; | ||
| HITCBC | 65 | 106453 | } | 88 | 136552 | } | ||
| 66 | 89 | |||||||
| 67 | /// Return true once the segment has been abandoned. | 90 | /// Return true once the segment has been abandoned. | |||||
| HITCBC | 68 | 7854 | bool abandoned() const noexcept | 91 | 32947 | bool abandoned() const noexcept | ||
| 69 | { | 92 | { | |||||
| HITCBC | 70 | - | 7854 | return abandoned_.load(std::memory_order_acquire); | 93 | + | 65894 | return cause_.load(std::memory_order_acquire) != not_abandoned; |
| 94 | + | } | ||||||
| 95 | + | |||||||
| 96 | + | /** Return true if the segment was abandoned because of a stop. | ||||||
| 97 | + | |||||||
| 98 | + | @return `true` only if the first abandon of the segment was | ||||||
| 99 | + | @ref abandon_for_stop. | ||||||
| 100 | + | */ | ||||||
| HITGNC | 101 | + | 30802 | bool stopped() const noexcept | ||||
| 102 | + | { | ||||||
| HITGNC | 103 | + | 61604 | return cause_.load(std::memory_order_acquire) == by_stop; | ||||
| 71 | } | 104 | } | |||||
| 72 | 105 | |||||||
| 73 | /** Unwind the segment on the owner's executor. | 106 | /** Unwind the segment on the owner's executor. | |||||
| 74 | 107 | |||||||
| 75 | Does nothing if the segment was already abandoned. Otherwise | 108 | Does nothing if the segment was already abandoned. Otherwise | |||||
| 76 | posts the owner's continuation; nothing of the owner is touched | 109 | posts the owner's continuation; nothing of the owner is touched | |||||
| 77 | afterward, since the post may unwind and free it at once. | 110 | afterward, since the post may unwind and free it at once. | |||||
| 78 | 111 | |||||||
| 79 | @note A post that throws calls `std::terminate`. | 112 | @note A post that throws calls `std::terminate`. | |||||
| 80 | */ | 113 | */ | |||||
| HITCBC | 81 | 101381 | void abandon() noexcept | 114 | 111375 | void abandon() noexcept | ||
| 82 | { | 115 | { | |||||
| HITCBC | 83 | - | 101381 | if(abandoned_.exchange(true, std::memory_order_acq_rel)) | 116 | + | 111375 | abandon_as(by_teardown); |
| HITCBC | 84 | - | 1 | return; | 117 | + | 111375 | } |
| ECB | 85 | - | 101380 | ex_.post(self_); | 118 | + | ||
| 119 | + | /** Unwind the segment on the owner's executor, as stopped. | ||||||
| 120 | + | |||||||
| 121 | + | The same as @ref abandon, but records the cause as a stop, so | ||||||
| 122 | + | @ref stopped returns `true` if this call was the first. Called | ||||||
| 123 | + | when a @ref quitter in the segment is stopped: at a `co_await` | ||||||
| 124 | + | in its body, when an operation completes into its segment's | ||||||
| 125 | + | gate, or when the gate through which `run_async` or `run` | ||||||
| 126 | + | starts a quitter does not start it. | ||||||
| 127 | + | |||||||
| 128 | + | @note A post that throws calls `std::terminate`. | ||||||
| 129 | + | */ | ||||||
| HITGNC | 130 | + | 14836 | void abandon_for_stop() noexcept | ||||
| 131 | + | { | ||||||
| HITGNC | 132 | + | 14836 | abandon_as(by_stop); | ||||
| HITGIC | 86 | } | 133 | 14836 | } | |||
| 87 | }; | 134 | }; | |||||
| 88 | 135 | |||||||
| 89 | /** Give up a suspended coroutine for good. | 136 | /** Give up a suspended coroutine for good. | |||||
| 90 | 137 | |||||||
| 91 | Called by the holder of a suspended coroutine that will never resume | 138 | Called by the holder of a suspended coroutine that will never resume | |||||
| 92 | it, such as an execution context, strand or primitive being torn | 139 | it, such as an execution context, strand or primitive being torn | |||||
| 93 | down. The coroutine's segment is unwound from its owner: on the | 140 | down. The coroutine's segment is unwound from its owner: on the | |||||
| 94 | owner's executor, from the top frame down to this coroutine, without | 141 | owner's executor, from the top frame down to this coroutine, without | |||||
| 95 | - | any frame delivering a result. Ordinary cancellation does not use | 142 | + | any frame delivering a result. Ordinary cancellation of a @ref task | |||
| 96 | - | this; it resumes the coroutine with an error instead. | 143 | + | does not use this; it resumes the coroutine with an error instead. | |||
| 144 | + | A @ref quitter's stop does use abandonment, through | ||||||
| 145 | + | @ref chain_owner::abandon_for_stop. | ||||||
| 97 | 146 | |||||||
| 98 | A continuation with no owner, such as an owner's own continuation, | 147 | A continuation with no owner, such as an owner's own continuation, | |||||
| 99 | is destroyed directly. | 148 | is destroyed directly. | |||||
| 100 | 149 | |||||||
| 101 | @par Preconditions | 150 | @par Preconditions | |||||
| 102 | `c` refers to a suspended coroutine that the caller holds and that | 151 | `c` refers to a suspended coroutine that the caller holds and that | |||||
| 103 | nothing else will resume or abandon. If `c` has no owner, nothing | 152 | nothing else will resume or abandon. If `c` has no owner, nothing | |||||
| 104 | awaits the coroutine: destroying its frame alone would leave any | 153 | awaits the coroutine: destroying its frame alone would leave any | |||||
| 105 | awaiting frame holding a destroyed one. | 154 | awaiting frame holding a destroyed one. | |||||
| 106 | 155 | |||||||
| 107 | @par Postconditions | 156 | @par Postconditions | |||||
| 108 | The caller must not touch the coroutine, or anything in its frame, | 157 | The caller must not touch the coroutine, or anything in its frame, | |||||
| 109 | again. The frames may already be destroyed when this returns, if the | 158 | again. The frames may already be destroyed when this returns, if the | |||||
| 110 | owner's executor destroys posted work at once. | 159 | owner's executor destroys posted work at once. | |||||
| 111 | 160 | |||||||
| 112 | @note The unwind runs on the owner's executor, which may belong to | 161 | @note The unwind runs on the owner's executor, which may belong to | |||||
| 113 | another execution context than the caller's, and may run after the | 162 | another execution context than the caller's, and may run after the | |||||
| 114 | caller's context is destroyed. A chain that holds objects of the | 163 | caller's context is destroyed. A chain that holds objects of the | |||||
| 115 | caller's context must therefore have finished before that context | 164 | caller's context must therefore have finished before that context | |||||
| 116 | is torn down. | 165 | is torn down. | |||||
| 117 | 166 | |||||||
| 118 | @note Each frame is destroyed from the destructor of the frame | 167 | @note Each frame is destroyed from the destructor of the frame | |||||
| 119 | awaiting it, so the unwind uses stack in proportion to how deeply | 168 | awaiting it, so the unwind uses stack in proportion to how deeply | |||||
| 120 | the chain's coroutines are nested. | 169 | the chain's coroutines are nested. | |||||
| 121 | 170 | |||||||
| 122 | @param c The continuation of the coroutine given up. | 171 | @param c The continuation of the coroutine given up. | |||||
| 123 | */ | 172 | */ | |||||
| 124 | inline void | 173 | inline void | |||||
| HITCBC | 125 | 203405 | abandon(continuation& c) noexcept | 174 | 203389 | abandon(continuation& c) noexcept | ||
| 126 | { | 175 | { | |||||
| HITCBC | 127 | 203405 | if(c.owner) | 176 | 203389 | if(c.owner) | ||
| HITCBC | 128 | 101379 | c.owner->abandon(); | 177 | 101372 | c.owner->abandon(); | ||
| HITCBC | 129 | 102026 | else if(c.h) | 178 | 102017 | else if(c.h) | ||
| HITCBC | 130 | 102026 | c.h.destroy(); | 179 | 102017 | c.h.destroy(); | ||
| HITCBC | 131 | 203405 | } | 180 | 203389 | } | ||
| 132 | 181 | |||||||
| 133 | } // namespace capy | 182 | } // namespace capy | |||||
| 134 | } // namespace boost | 183 | } // namespace boost | |||||
| 135 | 184 | |||||||
| 136 | #endif | 185 | #endif | |||||