include/boost/capy/ex/strand.hpp

100.0% Lines (25/25) 100.0% Functions (14/14)
Line TLA Hits 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 <coroutine>
15 #include <boost/capy/ex/detail/strand_service.hpp>
16
17 #include <type_traits>
18
19 namespace boost {
20 namespace capy {
21
22 /** Provides serialized coroutine execution for any executor type.
23
24 A strand wraps an inner executor and ensures that coroutines
25 dispatched through it never run concurrently. At most one
26 coroutine executes at a time within a strand, even when the
27 underlying executor runs on multiple threads.
28
29 Strands are lightweight handles that can be copied freely.
30 Copies share the same internal serialization state, so
31 coroutines dispatched through any copy are serialized with
32 respect to all other copies.
33
34 @par Invariant
35 Coroutines resumed through a strand shall not run concurrently.
36
37 @par Implementation
38 The strand uses a service-based architecture with a fixed pool
39 of 211 implementation objects. New strands hash to select an
40 impl from the pool. Strands that hash to the same index share
41 serialization, which is harmless (just extra serialization)
42 and rare with 211 buckets.
43
44 @par Executor Concept
45 This class satisfies the `Executor` concept, providing:
46 - `context()` - Returns the underlying execution context
47 - `on_work_started()` / `on_work_finished()` - Work tracking
48 - `dispatch(h)` - May run immediately if strand is idle
49 - `post(h)` - Always queues for later execution
50
51 @par Thread Safety
52 Distinct objects: Safe.
53 Shared objects: Safe.
54
55 @par Example
56 @code
57 thread_pool pool(4);
58 auto strand = make_strand(pool.get_executor());
59
60 // These coroutines will never run concurrently
61 strand.post(coro1);
62 strand.post(coro2);
63 strand.post(coro3);
64 @endcode
65
66 @tparam E The type of the underlying executor. Must
67 satisfy the `Executor` concept.
68
69 @see make_strand, Executor
70 */
71 template<typename Ex>
72 class strand
73 {
74 detail::strand_impl* impl_;
75 Ex ex_;
76
77 public:
78 /** The type of the underlying executor.
79 */
80 using inner_executor_type = Ex;
81
82 /** Construct a strand for the specified executor.
83
84 Obtains a strand implementation from the service associated
85 with the executor's context. The implementation is selected
86 from a fixed pool using a hash function.
87
88 @param ex The inner executor to wrap. Coroutines will
89 ultimately be dispatched through this executor.
90
91 @note This constructor is disabled if the argument is a
92 strand type, to prevent strand-of-strand wrapping.
93 */
94 template<typename Ex1,
95 typename = std::enable_if_t<
96 !std::is_same_v<std::decay_t<Ex1>, strand> &&
97 !detail::is_strand<std::decay_t<Ex1>>::value &&
98 std::is_convertible_v<Ex1, Ex>>>
99 explicit
100 25x strand(Ex1&& ex)
101 25x : impl_(detail::get_strand_service(ex.context())
102 25x .get_implementation())
103 25x , ex_(std::forward<Ex1>(ex))
104 {
105 25x }
106
107 /** Construct a copy.
108
109 Creates a strand that shares serialization state with
110 the original. Coroutines dispatched through either strand
111 will be serialized with respect to each other.
112 */
113 1x strand(strand const&) = default;
114
115 /** Construct by moving.
116
117 @note A moved-from strand is only safe to destroy
118 or reassign.
119 */
120 strand(strand&&) = default;
121
122 /** Assign by copying.
123 */
124 strand& operator=(strand const&) = default;
125
126 /** Assign by moving.
127
128 @note A moved-from strand is only safe to destroy
129 or reassign.
130 */
131 strand& operator=(strand&&) = default;
132
133 /** Return the underlying executor.
134
135 @return A const reference to the inner executor.
136 */
137 Ex const&
138 1x get_inner_executor() const noexcept
139 {
140 1x return ex_;
141 }
142
143 /** Return the underlying execution context.
144
145 @return A reference to the execution context associated
146 with the inner executor.
147 */
148 auto&
149 3x context() const noexcept
150 {
151 3x return ex_.context();
152 }
153
154 /** Notify that work has started.
155
156 Delegates to the inner executor's `on_work_started()`.
157 This is a no-op for most executor types.
158 */
159 void
160 4x on_work_started() const noexcept
161 {
162 4x ex_.on_work_started();
163 4x }
164
165 /** Notify that work has finished.
166
167 Delegates to the inner executor's `on_work_finished()`.
168 This is a no-op for most executor types.
169 */
170 void
171 4x on_work_finished() const noexcept
172 {
173 4x ex_.on_work_finished();
174 4x }
175
176 /** Determine whether the strand is running in the current thread.
177
178 @return true if the current thread is executing a coroutine
179 within this strand's dispatch loop.
180 */
181 bool
182 1x running_in_this_thread() const noexcept
183 {
184 1x return detail::strand_service::running_in_this_thread(*impl_);
185 }
186
187 /** Compare two strands for equality.
188
189 Two strands are equal if they share the same internal
190 serialization state. Equal strands serialize coroutines
191 with respect to each other.
192
193 @param other The strand to compare against.
194 @return true if both strands share the same implementation.
195 */
196 bool
197 4x operator==(strand const& other) const noexcept
198 {
199 4x return impl_ == other.impl_;
200 }
201
202 /** Post a coroutine to the strand.
203
204 The coroutine is always queued for execution, never resumed
205 immediately. When the strand becomes available, queued
206 coroutines execute in FIFO order on the underlying executor.
207
208 @par Ordering
209 Guarantees strict FIFO ordering relative to other post() calls.
210 Use this instead of dispatch() when ordering matters.
211
212 @param h The coroutine handle to post.
213 */
214 void
215 325x post(std::coroutine_handle<> h) const
216 {
217 325x detail::strand_service::post(*impl_, executor_ref(ex_), h);
218 325x }
219
220 /** Dispatch a coroutine through the strand.
221
222 Returns a handle for symmetric transfer. If the calling
223 thread is already executing within this strand, returns `h`.
224 Otherwise, the coroutine is queued and
225 `std::noop_coroutine()` is returned.
226
227 @par Ordering
228 Callers requiring strict FIFO ordering should use post()
229 instead, which always queues the coroutine.
230
231 @param h The coroutine handle to dispatch.
232
233 @return A handle for symmetric transfer or `std::noop_coroutine()`.
234 */
235 std::coroutine_handle<>
236 5x dispatch(std::coroutine_handle<> h) const
237 {
238 5x return detail::strand_service::dispatch(*impl_, executor_ref(ex_), h);
239 }
240 };
241
242 // Deduction guide
243 template<typename Ex>
244 strand(Ex) -> strand<Ex>;
245
246 } // namespace capy
247 } // namespace boost
248
249 #endif
250