TLA Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
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_STRAND_HPP
12 : #define BOOST_CAPY_EX_STRAND_HPP
13 :
14 : #include <boost/capy/detail/config.hpp>
15 : #include <boost/capy/continuation.hpp>
16 : #include <coroutine>
17 : #include <boost/capy/ex/detail/strand_service.hpp>
18 :
19 : #include <type_traits>
20 :
21 : namespace boost {
22 : namespace capy {
23 :
24 : /** Provides serialized coroutine execution for any executor type.
25 :
26 : A strand wraps an inner executor and ensures that coroutines
27 : dispatched through it never run concurrently. At most one
28 : coroutine executes at a time within a strand, even when the
29 : underlying executor runs on multiple threads.
30 :
31 : Strands are lightweight handles that can be copied freely.
32 : Copies share the same internal serialization state, so
33 : coroutines dispatched through any copy are serialized with
34 : respect to all other copies.
35 :
36 : @par Invariant
37 : Coroutines resumed through a strand shall not run concurrently.
38 :
39 : @par Implementation
40 : Each strand allocates a private serialization state. Strands
41 : constructed from the same execution context share a small pool
42 : of mutexes (193 entries) selected by hash. Mutex sharing causes
43 : only brief contention on the push/pop critical section, never
44 : cross-strand state sharing. Construction cost: one
45 : `std::make_shared` per strand.
46 :
47 : @par Executor Concept
48 : This class satisfies the `Executor` concept, providing:
49 : - `context()` - Returns the underlying execution context
50 : - `on_work_started()` / `on_work_finished()` - Work tracking
51 : - `dispatch(continuation&)` - May run immediately if already executing in this strand
52 : - `post(continuation&)` - Always queues for later execution
53 :
54 : @par Preconditions
55 : A strand holds only a non-owning reference to its inner executor's
56 : execution context (for example a `thread_pool`). That context must
57 : outlive every post() and dispatch() call; posting or dispatching
58 : concurrently with, or after, the context's destruction is undefined
59 : behavior. To guarantee this, submit work through @ref run_async or
60 : @ref run. Their operations are work-tracked, so the context's
61 : `join()` waits for them. Call `join()` on the context before
62 : destroying it, rather than posting to a strand from an external
63 : thread the context does not track. Destroying the strand handle
64 : itself is always safe, including after the context is
65 : destroyed.
66 :
67 : @par Thread Safety
68 : Distinct objects: Safe.
69 : Shared objects: Safe.
70 :
71 : @par Example
72 : @code
73 : thread_pool pool(4);
74 : strand strand(pool.get_executor()); // CTAD deduces the executor type
75 :
76 : // Continuations are linked intrusively into the strand's queue,
77 : // so each one must outlive its time there. Storage is typically
78 : // owned by the awaitable or operation state that posted it.
79 : continuation c1{h1}, c2{h2}, c3{h3};
80 : strand.post(c1);
81 : strand.post(c2);
82 : strand.post(c3);
83 : @endcode
84 :
85 : @tparam Ex The type of the underlying executor. Must
86 : satisfy the `Executor` concept.
87 :
88 : @see Executor
89 : */
90 : template<typename Ex>
91 : class strand
92 : {
93 : std::shared_ptr<detail::strand_impl> impl_;
94 : Ex ex_;
95 :
96 : friend struct strand_test;
97 :
98 : public:
99 : /** Names the executor type this `strand<Ex>` wraps.
100 : */
101 : using inner_executor_type = Ex;
102 :
103 : /** Construct a strand for the specified executor.
104 :
105 : Allocates a fresh strand implementation from the service
106 : associated with the executor's context.
107 :
108 : @param ex The inner executor to wrap. Coroutines are
109 : ultimately dispatched through this executor.
110 :
111 : @note This constructor is disabled if the argument is a
112 : strand type, to prevent strand-of-strand wrapping.
113 : */
114 : template<typename Ex1,
115 : typename = std::enable_if_t<
116 : !std::is_same_v<std::decay_t<Ex1>, strand> &&
117 : !detail::is_strand<std::decay_t<Ex1>>::value &&
118 : std::is_convertible_v<Ex1, Ex>>>
119 : explicit
120 HIT 11446 : strand(Ex1&& ex)
121 11446 : : impl_(detail::get_strand_service(ex.context())
122 11446 : .create_implementation())
123 11446 : , ex_(std::forward<Ex1>(ex))
124 : {
125 11446 : }
126 :
127 : /** Construct a copy.
128 :
129 : Creates a strand that shares serialization state with
130 : the original. Coroutines dispatched through either strand
131 : are serialized with respect to each other.
132 :
133 : @param other The strand to copy.
134 : */
135 11 : strand(strand const& other) = default;
136 :
137 : /** Construct by moving.
138 :
139 : @param other The strand to move from.
140 :
141 : @note A moved-from strand is only safe to destroy
142 : or reassign.
143 : */
144 11453 : strand(strand&& other) = default;
145 :
146 : /** Assign by copying.
147 :
148 : Shares serialization state with `other`, as the copy
149 : constructor does.
150 :
151 : @param other The strand to copy.
152 :
153 : @return A reference to `*this`.
154 : */
155 1 : strand& operator=(strand const& other) = default;
156 :
157 : /** Assign by moving.
158 :
159 : @param other The strand to move from.
160 :
161 : @return A reference to `*this`.
162 :
163 : @note A moved-from strand is only safe to destroy
164 : or reassign.
165 : */
166 1 : strand& operator=(strand&& other) = default;
167 :
168 : /** Return the underlying executor.
169 :
170 : @return A const reference to the inner executor.
171 : */
172 : Ex const&
173 1 : get_inner_executor() const noexcept
174 : {
175 1 : return ex_;
176 : }
177 :
178 : /** Return the underlying execution context.
179 :
180 : @return A reference to the execution context associated
181 : with the inner executor.
182 : */
183 : auto&
184 6 : context() const noexcept
185 : {
186 6 : return ex_.context();
187 : }
188 :
189 : /** Notify that work has started.
190 :
191 : Delegates to the inner executor's `on_work_started()`. For a
192 : `thread_pool` inner executor, this increments the count that
193 : `join()` blocks on.
194 : */
195 : void
196 7 : on_work_started() const noexcept
197 : {
198 7 : ex_.on_work_started();
199 7 : }
200 :
201 : /** Notify that work has finished.
202 :
203 : Delegates to the inner executor's `on_work_finished()`. For a
204 : `thread_pool` inner executor, this decrements the count that
205 : `join()` blocks on.
206 : */
207 : void
208 7 : on_work_finished() const noexcept
209 : {
210 7 : ex_.on_work_finished();
211 7 : }
212 :
213 : /** Determine whether the strand is running in the current thread.
214 :
215 : @return true if the current thread is executing a coroutine
216 : within this strand's dispatch loop.
217 : */
218 : bool
219 4 : running_in_this_thread() const noexcept
220 : {
221 4 : return detail::strand_service::running_in_this_thread(*impl_);
222 : }
223 :
224 : /** Compare two strands for equality.
225 :
226 : Two strands are equal if they share the same internal
227 : serialization state. Equal strands serialize coroutines
228 : with respect to each other.
229 :
230 : @param other The strand to compare against.
231 : @return true if both strands share the same implementation.
232 : */
233 : bool
234 499505 : operator==(strand const& other) const noexcept
235 : {
236 499505 : return impl_.get() == other.impl_.get();
237 : }
238 :
239 : /** Post a continuation to the strand.
240 :
241 : The continuation is always queued for execution, never resumed
242 : immediately. When the strand becomes available, queued
243 : work executes in FIFO order on the underlying executor.
244 :
245 : @par Ordering
246 : Guarantees strict FIFO ordering relative to other post() calls.
247 : Use this instead of dispatch() when ordering matters.
248 :
249 : @param c The continuation to post. The caller retains
250 : ownership; the continuation must remain valid until
251 : it is dequeued and resumed.
252 :
253 : @par Preconditions
254 : The strand's execution context must outlive this call. Posting
255 : concurrently with, or after, that context's destruction is
256 : undefined behavior.
257 : */
258 : void
259 30336 : post(continuation& c) const
260 : {
261 30336 : detail::strand_service::post(impl_, executor_ref(ex_), c);
262 30336 : }
263 :
264 : /** Dispatch a continuation through the strand.
265 :
266 : Returns a handle for symmetric transfer. If the calling
267 : thread is already executing within this strand, returns `c.h`.
268 : Otherwise, the continuation is queued and
269 : `std::noop_coroutine()` is returned.
270 :
271 : @par Ordering
272 : Callers requiring strict FIFO ordering should use post()
273 : instead, which always queues the continuation.
274 :
275 : @param c The continuation to dispatch. The caller retains
276 : ownership; the continuation must remain valid until
277 : it is dequeued and resumed.
278 :
279 : @return A handle for symmetric transfer or `std::noop_coroutine()`.
280 :
281 : @par Preconditions
282 : The strand's execution context must outlive this call.
283 : Dispatching concurrently with, or after, that context's
284 : destruction is undefined behavior.
285 : */
286 : std::coroutine_handle<>
287 9 : dispatch(continuation& c) const
288 : {
289 9 : return detail::strand_service::dispatch(impl_, executor_ref(ex_), c);
290 : }
291 : };
292 :
293 : /** Deduce the executor type from the constructor argument.
294 :
295 : @tparam Ex The wrapped executor type.
296 : */
297 : template<typename Ex>
298 : strand(Ex) -> strand<Ex>;
299 :
300 : } // namespace capy
301 : } // namespace boost
302 :
303 : #endif
|