TLA Line data 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 HIT 126211 : void abandon_as(unsigned char cause) noexcept
64 : {
65 126211 : unsigned char expected = not_abandoned;
66 252422 : if(!cause_.compare_exchange_strong(
67 : expected, cause,
68 : std::memory_order_acq_rel,
69 : std::memory_order_acquire))
70 10002 : return;
71 116209 : 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 136552 : void bind(std::coroutine_handle<> self, executor_ref ex) noexcept
85 : {
86 136552 : self_.h = self;
87 136552 : ex_ = ex;
88 136552 : }
89 :
90 : /// Return true once the segment has been abandoned.
91 32947 : bool abandoned() const noexcept
92 : {
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 : */
101 30802 : bool stopped() const noexcept
102 : {
103 61604 : 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 111375 : void abandon() noexcept
115 : {
116 111375 : abandon_as(by_teardown);
117 111375 : }
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 14836 : void abandon_for_stop() noexcept
131 : {
132 14836 : abandon_as(by_stop);
133 14836 : }
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 203389 : abandon(continuation& c) noexcept
175 : {
176 203389 : if(c.owner)
177 101372 : c.owner->abandon();
178 102017 : else if(c.h)
179 102017 : c.h.destroy();
180 203389 : }
181 :
182 : } // namespace capy
183 : } // namespace boost
184 :
185 : #endif
|