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