include/boost/capy/ex/abandon.hpp

100.0% Lines (25 / 25) 100.0% Functions (7 / 7)
abandon.hpp
f(x) Functions (7)
Line TLA Hits Source Code
1 //
2 // Copyright (c) 2026 Steve Gerbino
3 // Copyright (c) 2026 Michael Vandeberg
4 //
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)
7 //
8 // Official repository: https://github.com/cppalliance/capy
9 //
10
11 #ifndef BOOST_CAPY_EX_ABANDON_HPP
12 #define BOOST_CAPY_EX_ABANDON_HPP
13
14 #include <boost/capy/detail/config.hpp>
15 #include <boost/capy/continuation.hpp>
16 #include <boost/capy/ex/executor_ref.hpp>
17
18 #include <atomic>
19 #include <coroutine>
20
21 namespace boost {
22 namespace capy {
23
24 /** The owner of one segment of a coroutine chain.
25
26 A segment is a run of coroutines started together: the task
27 `run_async` launches and everything it awaits, a task `run` starts
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
30 owner is the coroutine that started it, suspended above the
31 segment's top frame until that frame completes. Every coroutine in
32 the segment reaches the owner through its @ref await_context.
33
34 When the holder of a suspended coroutine in the segment gives it up
35 with @ref abandon, the owner posts its own continuation to its
36 executor. There it unwinds the segment from the top: it destroys
37 the frame it awaits, which destroys the frames below it in turn.
38 After this teardown, the owner delivers no result. An executor that
39 destroys the posted continuation instead, as a closed one does,
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.
49
50 @par Thread Safety
51 @ref abandon and @ref abandon_for_stop may be called from any
52 thread. Only the first call takes effect, and decides the cause; a
53 later call of either before the unwind runs does nothing.
54 */
55 class chain_owner
56 {
57 enum : unsigned char { not_abandoned, by_teardown, by_stop };
58
59 std::atomic<unsigned char> cause_{not_abandoned};
60 continuation self_;
61 executor_ref ex_;
62
63 126211x void abandon_as(unsigned char cause) noexcept
64 {
65 126211x unsigned char expected = not_abandoned;
66 252422x if(!cause_.compare_exchange_strong(
67 expected, cause,
68 std::memory_order_acq_rel,
69 std::memory_order_acquire))
70 10002x return;
71 116209x ex_.post(self_);
72 }
73
74 public:
75 /** Bind the owner to its coroutine and executor.
76
77 Called by the owner before it starts the segment.
78
79 @param self The owner's coroutine, resumed or destroyed to
80 unwind the segment.
81
82 @param ex The executor the segment runs on.
83 */
84 136552x void bind(std::coroutine_handle<> self, executor_ref ex) noexcept
85 {
86 136552x self_.h = self;
87 136552x ex_ = ex;
88 136552x }
89
90 /// Return true once the segment has been abandoned.
91 32947x bool abandoned() const noexcept
92 {
93 65894x 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 */
101 30802x bool stopped() const noexcept
102 {
103 61604x return cause_.load(std::memory_order_acquire) == by_stop;
104 }
105
106 /** Unwind the segment on the owner's executor.
107
108 Does nothing if the segment was already abandoned. Otherwise
109 posts the owner's continuation; nothing of the owner is touched
110 afterward, since the post may unwind and free it at once.
111
112 @note A post that throws calls `std::terminate`.
113 */
114 111375x void abandon() noexcept
115 {
116 111375x abandon_as(by_teardown);
117 111375x }
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 */
130 14836x void abandon_for_stop() noexcept
131 {
132 14836x abandon_as(by_stop);
133 14836x }
134 };
135
136 /** Give up a suspended coroutine for good.
137
138 Called by the holder of a suspended coroutine that will never resume
139 it, such as an execution context, strand or primitive being torn
140 down. The coroutine's segment is unwound from its owner: on the
141 owner's executor, from the top frame down to this coroutine, without
142 any frame delivering a result. Ordinary cancellation of a @ref task
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.
146
147 A continuation with no owner, such as an owner's own continuation,
148 is destroyed directly.
149
150 @par Preconditions
151 `c` refers to a suspended coroutine that the caller holds and that
152 nothing else will resume or abandon. If `c` has no owner, nothing
153 awaits the coroutine: destroying its frame alone would leave any
154 awaiting frame holding a destroyed one.
155
156 @par Postconditions
157 The caller must not touch the coroutine, or anything in its frame,
158 again. The frames may already be destroyed when this returns, if the
159 owner's executor destroys posted work at once.
160
161 @note The unwind runs on the owner's executor, which may belong to
162 another execution context than the caller's, and may run after the
163 caller's context is destroyed. A chain that holds objects of the
164 caller's context must therefore have finished before that context
165 is torn down.
166
167 @note Each frame is destroyed from the destructor of the frame
168 awaiting it, so the unwind uses stack in proportion to how deeply
169 the chain's coroutines are nested.
170
171 @param c The continuation of the coroutine given up.
172 */
173 inline void
174 203389x abandon(continuation& c) noexcept
175 {
176 203389x if(c.owner)
177 101372x c.owner->abandon();
178 102017x else if(c.h)
179 102017x c.h.destroy();
180 203389x }
181
182 } // namespace capy
183 } // namespace boost
184
185 #endif
186