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 <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 HIT 25 : strand(Ex1&& ex)
101 25 : : impl_(detail::get_strand_service(ex.context())
102 25 : .get_implementation())
103 25 : , ex_(std::forward<Ex1>(ex))
104 : {
105 25 : }
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 1 : 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 1 : get_inner_executor() const noexcept
139 : {
140 1 : 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 3 : context() const noexcept
150 : {
151 3 : 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 4 : on_work_started() const noexcept
161 : {
162 4 : ex_.on_work_started();
163 4 : }
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 4 : on_work_finished() const noexcept
172 : {
173 4 : ex_.on_work_finished();
174 4 : }
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 1 : running_in_this_thread() const noexcept
183 : {
184 1 : 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 4 : operator==(strand const& other) const noexcept
198 : {
199 4 : 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 325 : post(std::coroutine_handle<> h) const
216 : {
217 325 : detail::strand_service::post(*impl_, executor_ref(ex_), h);
218 325 : }
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 5 : dispatch(std::coroutine_handle<> h) const
237 : {
238 5 : 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
|