100.00% Lines (98/98) 100.00% Functions (27/27)
TLA Baseline Branch
Line Hits Code Line Hits Code
1   // 1   //
2   // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) 2   // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3   // Copyright (c) 2026 Steve Gerbino 3   // Copyright (c) 2026 Steve Gerbino
4   // Copyright (c) 2026 Michael Vandeberg 4   // Copyright (c) 2026 Michael Vandeberg
5   // 5   //
6   // Distributed under the Boost Software License, Version 1.0. (See accompanying 6   // Distributed under the Boost Software License, Version 1.0. (See accompanying
7   // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) 7   // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
8   // 8   //
9   // Official repository: https://github.com/cppalliance/corosio 9   // Official repository: https://github.com/cppalliance/corosio
10   // 10   //
11   11  
12   #ifndef BOOST_COROSIO_IO_CONTEXT_HPP 12   #ifndef BOOST_COROSIO_IO_CONTEXT_HPP
13   #define BOOST_COROSIO_IO_CONTEXT_HPP 13   #define BOOST_COROSIO_IO_CONTEXT_HPP
14   14  
15   #include <boost/corosio/detail/config.hpp> 15   #include <boost/corosio/detail/config.hpp>
16   #include <boost/corosio/detail/platform.hpp> 16   #include <boost/corosio/detail/platform.hpp>
17   #include <boost/corosio/detail/scheduler.hpp> 17   #include <boost/corosio/detail/scheduler.hpp>
18   #include <boost/capy/continuation.hpp> 18   #include <boost/capy/continuation.hpp>
19   #include <boost/capy/ex/execution_context.hpp> 19   #include <boost/capy/ex/execution_context.hpp>
20   #include <boost/capy/ex/frame_allocator.hpp> 20   #include <boost/capy/ex/frame_allocator.hpp>
21   21  
22   #include <chrono> 22   #include <chrono>
23   #include <coroutine> 23   #include <coroutine>
24   #include <cstddef> 24   #include <cstddef>
25   #include <limits> 25   #include <limits>
26   #include <thread> 26   #include <thread>
27   27  
28   namespace boost::corosio { 28   namespace boost::corosio {
29   29  
30   /** Selects which internal locks the scheduler and reactor elide, 30   /** Selects which internal locks the scheduler and reactor elide,
31   trading thread-safety guarantees for reduced synchronization 31   trading thread-safety guarantees for reduced synchronization
32   overhead. 32   overhead.
33   33  
34   This is the analog of Boost.Asio's `SAFE` / `UNSAFE_IO` / `UNSAFE` 34   This is the analog of Boost.Asio's `SAFE` / `UNSAFE_IO` / `UNSAFE`
35   concurrency hint constants. The tier is chosen explicitly, not derived 35   concurrency hint constants. The tier is chosen explicitly, not derived
36   from the `concurrency_hint`. (The reverse does apply: a lockless tier 36   from the `concurrency_hint`. (The reverse does apply: a lockless tier
37   reduces the effective hint used for performance tuning to 1.) 37   reduces the effective hint used for performance tuning to 1.)
38   38  
39 - @see @ref io_context_options::locking 39 + @see io_context_options::locking
40   */ 40   */
41   enum class locking_mode 41   enum class locking_mode
42   { 42   {
43   /** Full thread safety (default). All locks enabled; equivalent to 43   /** Full thread safety (default). All locks enabled; equivalent to
44   Boost.Asio's `SAFE`/`DEFAULT`. Any thread may use the context. */ 44   Boost.Asio's `SAFE`/`DEFAULT`. Any thread may use the context. */
45   safe, 45   safe,
46   46  
47   /** Disable only the per-descriptor I/O locks; keep scheduler locking. 47   /** Disable only the per-descriptor I/O locks; keep scheduler locking.
48   Equivalent to Boost.Asio's `UNSAFE_IO`. A single thread must run 48   Equivalent to Boost.Asio's `UNSAFE_IO`. A single thread must run
49   and drive the context. Resolver and POSIX file services remain 49   and drive the context. Resolver and POSIX file services remain
50   available, because they rely on scheduler locking, which stays 50   available, because they rely on scheduler locking, which stays
51   on. */ 51   on. */
52   unsafe_io, 52   unsafe_io,
53   53  
54   /** Disable all locking (fully lockless). Equivalent to Boost.Asio's 54   /** Disable all locking (fully lockless). Equivalent to Boost.Asio's
55   `UNSAFE`. 55   `UNSAFE`.
56   56  
57   @par Restrictions 57   @par Restrictions
58   - Only one thread may call `run()` (or any run variant). 58   - Only one thread may call `run()` (or any run variant).
59   - Posting work from another thread is undefined behavior. 59   - Posting work from another thread is undefined behavior.
60   - DNS resolution returns `operation_not_supported`, except on 60   - DNS resolution returns `operation_not_supported`, except on
61   IOCP, whose resolver does not use the thread pool. 61   IOCP, whose resolver does not use the thread pool.
62   - POSIX file I/O returns `operation_not_supported`. 62   - POSIX file I/O returns `operation_not_supported`.
63   - `win_object_handle::assign()` returns `operation_not_supported`. 63   - `win_object_handle::assign()` returns `operation_not_supported`.
64   - Signal sets should not be shared across contexts. */ 64   - Signal sets should not be shared across contexts. */
65   unsafe 65   unsafe
66   }; 66   };
67   67  
68   /** Configures scheduler and reactor tuning for an @ref io_context. 68   /** Configures scheduler and reactor tuning for an @ref io_context.
69   69  
70   All fields have defaults that match the library's built-in 70   All fields have defaults that match the library's built-in
71   values, so constructing a default `io_context_options` produces 71   values, so constructing a default `io_context_options` produces
72   identical behavior to an unconfigured context. 72   identical behavior to an unconfigured context.
73   73  
74   Options that apply only to a specific backend family are 74   Options that apply only to a specific backend family are
75   silently ignored when the active backend does not support them. 75   silently ignored when the active backend does not support them.
76   76  
77   @par Example 77   @par Example
78   @par !example configure 78   @par !example configure
79   79  
80 - @see @ref io_context, @ref native_io_context 80 + @see io_context, native_io_context
81   */ 81   */
82   struct io_context_options 82   struct io_context_options
83   { 83   {
84   /** Maximum events fetched per reactor poll call. 84   /** Maximum events fetched per reactor poll call.
85   85  
86   Controls the buffer size passed to `epoll_wait()` or 86   Controls the buffer size passed to `epoll_wait()` or
87   `kevent()`. Larger values reduce syscall frequency under 87   `kevent()`. Larger values reduce syscall frequency under
88   high load. Smaller values improve fairness between 88   high load. Smaller values improve fairness between
89   connections. Ignored on IOCP and select backends. 89   connections. Ignored on IOCP and select backends.
90   */ 90   */
91   unsigned max_events_per_poll = 128; 91   unsigned max_events_per_poll = 128;
92   92  
93   /** Starting inline completion budget per handler chain. 93   /** Starting inline completion budget per handler chain.
94   94  
95   After a posted handler executes, the reactor grants this 95   After a posted handler executes, the reactor grants this
96   many speculative inline completions before forcing a 96   many speculative inline completions before forcing a
97   re-queue. Applies to reactor backends only. 97   re-queue. Applies to reactor backends only.
98   98  
99   @note Constructing an `io_context` with `concurrency_hint > 1` 99   @note Constructing an `io_context` with `concurrency_hint > 1`
100   and all three budget fields at their defaults overrides them to 100   and all three budget fields at their defaults overrides them to
101   disable inline completion, giving post-everything mode. 101   disable inline completion, giving post-everything mode.
102   Multi-thread workloads benefit from cross-thread work-stealing. 102   Multi-thread workloads benefit from cross-thread work-stealing.
103   Setting any budget field to a non-default 103   Setting any budget field to a non-default
104   value disables the override. 104   value disables the override.
105   */ 105   */
106   unsigned inline_budget_initial = 2; 106   unsigned inline_budget_initial = 2;
107   107  
108   /** Hard ceiling on adaptive inline budget ramp-up. 108   /** Hard ceiling on adaptive inline budget ramp-up.
109   109  
110   The budget doubles each cycle it is fully consumed, up to 110   The budget doubles each cycle it is fully consumed, up to
111   this limit. Applies to reactor backends only. 111   this limit. Applies to reactor backends only.
112   */ 112   */
113   unsigned inline_budget_max = 16; 113   unsigned inline_budget_max = 16;
114   114  
115   /** Inline budget when no other thread assists the reactor. 115   /** Inline budget when no other thread assists the reactor.
116   116  
117   When only one thread is running the event loop, this 117   When only one thread is running the event loop, this
118   value caps the inline budget to preserve fairness. 118   value caps the inline budget to preserve fairness.
119   Applies to reactor backends only. 119   Applies to reactor backends only.
120   */ 120   */
121   unsigned unassisted_budget = 4; 121   unsigned unassisted_budget = 4;
122   122  
123   /** Thread pool size for blocking I/O (file I/O, DNS resolution). 123   /** Thread pool size for blocking I/O (file I/O, DNS resolution).
124   124  
125   Sets the number of worker threads in the shared thread pool 125   Sets the number of worker threads in the shared thread pool
126   used by POSIX file services and DNS resolution. Must be at 126   used by POSIX file services and DNS resolution. Must be at
127   least 1. Applies to POSIX backends only; ignored on IOCP 127   least 1. Applies to POSIX backends only; ignored on IOCP
128   where file I/O uses native overlapped I/O. 128   where file I/O uses native overlapped I/O.
129   */ 129   */
130   unsigned thread_pool_size = 1; 130   unsigned thread_pool_size = 1;
131   131  
132   /** Thread-safety tier. See @ref locking_mode for the tiers and their 132   /** Thread-safety tier. See @ref locking_mode for the tiers and their
133   restrictions. 133   restrictions.
134   */ 134   */
135   locking_mode locking = locking_mode::safe; 135   locking_mode locking = locking_mode::safe;
136   136  
137   /** Enable IORING_SETUP_SQPOLL on the io_uring backend. 137   /** Enable IORING_SETUP_SQPOLL on the io_uring backend.
138   138  
139   With SQPOLL, the kernel forks a thread that busy-polls the 139   With SQPOLL, the kernel forks a thread that busy-polls the
140   submission ring. Submission becomes a userspace-only memory 140   submission ring. Submission becomes a userspace-only memory
141   store, which eliminates the `io_uring_enter` syscall on the submit 141   store, which eliminates the `io_uring_enter` syscall on the submit
142   path. Most useful for sustained traffic. Idle thread parks 142   path. Most useful for sustained traffic. Idle thread parks
143   after `sq_thread_idle_ms` of no activity. 143   after `sq_thread_idle_ms` of no activity.
144   144  
145   Independent of `locking`. Default: off. 145   Independent of `locking`. Default: off.
146   146  
147   Ignored on non-io_uring backends. 147   Ignored on non-io_uring backends.
148   */ 148   */
149   bool enable_sqpoll = false; 149   bool enable_sqpoll = false;
150   150  
151   /** SQ-poll idle timeout in milliseconds. 151   /** SQ-poll idle timeout in milliseconds.
152   152  
153   After this many ms of no submissions, the kernel polling 153   After this many ms of no submissions, the kernel polling
154   thread sleeps. The next submit re-wakes it via SQ_WAKEUP. 0 154   thread sleeps. The next submit re-wakes it via SQ_WAKEUP. 0
155   means use the kernel default (1ms). Recommended for bursty 155   means use the kernel default (1ms). Recommended for bursty
156   workloads: 100-1000ms (avoids park/unpark thrash). 156   workloads: 100-1000ms (avoids park/unpark thrash).
157   157  
158   Ignored unless `enable_sqpoll` is true. Ignored on 158   Ignored unless `enable_sqpoll` is true. Ignored on
159   non-io_uring backends. 159   non-io_uring backends.
160   */ 160   */
161   unsigned sq_thread_idle_ms = 0; 161   unsigned sq_thread_idle_ms = 0;
162   162  
163   /** Pin the SQ-poll kernel thread to this CPU. 163   /** Pin the SQ-poll kernel thread to this CPU.
164   164  
165   -1 means do not pin (kernel scheduler picks). Pinning off 165   -1 means do not pin (kernel scheduler picks). Pinning off
166   the dispatch core is recommended on latency-sensitive 166   the dispatch core is recommended on latency-sensitive
167   deployments to avoid cache contention. 167   deployments to avoid cache contention.
168   168  
169   Ignored unless `enable_sqpoll` is true. Ignored on 169   Ignored unless `enable_sqpoll` is true. Ignored on
170   non-io_uring backends. 170   non-io_uring backends.
171   */ 171   */
172   int sq_thread_cpu = -1; 172   int sq_thread_cpu = -1;
173   }; 173   };
174   174  
175   namespace detail { 175   namespace detail {
176   class timer_service; 176   class timer_service;
177   177  
178   /** Return the hint used for performance tuning: the lockless tiers are 178   /** Return the hint used for performance tuning: the lockless tiers are
179   single-threaded, so their effective hint is 1 whatever the caller passed. 179   single-threaded, so their effective hint is 1 whatever the caller passed.
180   */ 180   */
181   inline unsigned 181   inline unsigned
HITCBC 182   62 effective_concurrency_hint( 182   62 effective_concurrency_hint(
183   io_context_options const& opts, unsigned hint) noexcept 183   io_context_options const& opts, unsigned hint) noexcept
184   { 184   {
HITCBC 185   62 return opts.locking == locking_mode::safe ? hint : 1u; 185   62 return opts.locking == locking_mode::safe ? hint : 1u;
186   } 186   }
187   } // namespace detail 187   } // namespace detail
188   188  
189   /** Runs asynchronous operations and owns the I/O backend that drives them. 189   /** Runs asynchronous operations and owns the I/O backend that drives them.
190   190  
191   The `io_context` provides an execution environment for async 191   The `io_context` provides an execution environment for async
192   operations. It maintains a queue of pending work items and 192   operations. It maintains a queue of pending work items and
193   processes them when `run()` is called. 193   processes them when `run()` is called.
194   194  
195   The default and unsigned constructors select the platform's 195   The default and unsigned constructors select the platform's
196   native backend: 196   native backend:
197   - Windows: IOCP 197   - Windows: IOCP
198   - Linux: epoll 198   - Linux: epoll
199   - BSD/macOS: kqueue 199   - BSD/macOS: kqueue
200   - Other POSIX: select 200   - Other POSIX: select
201   201  
202   The template constructor accepts a backend tag value to 202   The template constructor accepts a backend tag value to
203   choose a specific backend at compile time: 203   choose a specific backend at compile time:
204   204  
205   @par Example 205   @par Example
206   @par !example construct 206   @par !example construct
207   207  
208   @pre The context must outlive every operation posted or dispatched 208   @pre The context must outlive every operation posted or dispatched
209   through its executor. No thread may be executing a run variant when 209   through its executor. No thread may be executing a run variant when
210   the context is destroyed. Posting to the context 210   the context is destroyed. Posting to the context
211   concurrently with, or after, its destruction is undefined 211   concurrently with, or after, its destruction is undefined
212   behavior. For a safe teardown, first stop submitting new work. 212   behavior. For a safe teardown, first stop submitting new work.
213   Then let every `run()` call return; each returns once no 213   Then let every `run()` call return; each returns once no
214   outstanding work remains. Finally join the threads that ran the 214   outstanding work remains. Finally join the threads that ran the
215   loop. Only then destroy the context. Work started with 215   loop. Only then destroy the context. Work started with
216   `capy::run` / `capy::run_async` is work-tracked, so a normal 216   `capy::run` / `capy::run_async` is work-tracked, so a normal
217   `run()` completion already waits for it. Destroying the context 217   `run()` completion already waits for it. Destroying the context
218   abandons every coroutine still suspended in it (see 218   abandons every coroutine still suspended in it (see
219   @ref ~io_context). 219   @ref ~io_context).
220   220  
221   @par Exception Safety 221   @par Exception Safety
222   A context that constructs is usable. The infrastructure its backend 222   A context that constructs is usable. The infrastructure its backend
223   needs — the completion port, the ring, the reactor's wakeup channel 223   needs — the completion port, the ring, the reactor's wakeup channel
224   — is created during construction. A system that refuses it therefore 224   — is created during construction. A system that refuses it therefore
225   throws from the constructor rather than from the first operation. 225   throws from the constructor rather than from the first operation.
226   The failed construction leaves nothing open. 226   The failed construction leaves nothing open.
227   227  
228   An operation that completes resumes its coroutine through the 228   An operation that completes resumes its coroutine through the
229   executor the coroutine awaited it on. If that executor's `post` or 229   executor the coroutine awaited it on. If that executor's `post` or
230   `dispatch` throws, `std::terminate` is called. 230   `dispatch` throws, `std::terminate` is called.
231   231  
232   @par Thread Safety 232   @par Thread Safety
233   Distinct objects: Safe.@n 233   Distinct objects: Safe.@n
234   Shared objects: Safe, unless the context was constructed with a 234   Shared objects: Safe, unless the context was constructed with a
235   lockless @ref io_context_options::locking tier (`unsafe_io` or 235   lockless @ref io_context_options::locking tier (`unsafe_io` or
236   `unsafe`), in which case a single thread must drive it. 236   `unsafe`), in which case a single thread must drive it.
237   237  
238 - @see @ref epoll_t, @ref select_t, @ref kqueue_t, @ref iocp_t 238 + @see epoll_t, select_t, kqueue_t, iocp_t
239   */ 239   */
240   class BOOST_COROSIO_DECL io_context : public capy::execution_context 240   class BOOST_COROSIO_DECL io_context : public capy::execution_context
241   { 241   {
242   /// Reject invalid options before the backend is constructed. 242   /// Reject invalid options before the backend is constructed.
243   void apply_options_pre_(io_context_options const& opts); 243   void apply_options_pre_(io_context_options const& opts);
244   244  
245   /** Create the blocking-I/O thread pool, apply runtime tuning to the 245   /** Create the blocking-I/O thread pool, apply runtime tuning to the
246   scheduler and finish bringing the backend up. The tail of every 246   scheduler and finish bringing the backend up. The tail of every
247   options constructor. The backend infrastructure whose setup reads 247   options constructor. The backend infrastructure whose setup reads
248   these options is created here, so a failure to create it throws 248   these options is created here, so a failure to create it throws
249   from the constructor. */ 249   from the constructor. */
250   void apply_options_post_( 250   void apply_options_post_(
251   io_context_options const& opts, unsigned concurrency_hint); 251   io_context_options const& opts, unsigned concurrency_hint);
252   252  
253   /** Create the blocking-I/O thread pool and apply only the decomposed 253   /** Create the blocking-I/O thread pool and apply only the decomposed
254   threading configuration (locking tiers), then finish bringing the 254   threading configuration (locking tiers), then finish bringing the
255   backend up. The tail of every plain constructor. Unlike the 255   backend up. The tail of every plain constructor. Unlike the
256   options constructors, it deliberately leaves the reactor budget 256   options constructors, it deliberately leaves the reactor budget
257   at its defaults rather than engaging the multi-thread 257   at its defaults rather than engaging the multi-thread
258   post-everything heuristic. */ 258   post-everything heuristic. */
259   void apply_threading_(io_context_options const& opts); 259   void apply_threading_(io_context_options const& opts);
260   260  
261   protected: 261   protected:
262   detail::scheduler* sched_; 262   detail::scheduler* sched_;
263   263  
264   // Resuming a chain installs its frame allocator on the running 264   // Resuming a chain installs its frame allocator on the running
265   // thread; a run call puts back what it found, so a coroutine created 265   // thread; a run call puts back what it found, so a coroutine created
266   // afterwards never reaches a resource that died with those frames. 266   // afterwards never reaches a resource that died with those frames.
267   struct frame_allocator_restore 267   struct frame_allocator_restore
268   { 268   {
269   std::pmr::memory_resource* saved = 269   std::pmr::memory_resource* saved =
270   capy::get_current_frame_allocator(); 270   capy::get_current_frame_allocator();
271   271  
HITCBC 272   11654 frame_allocator_restore() = default; 272   11540 frame_allocator_restore() = default;
273   frame_allocator_restore(frame_allocator_restore const&) = delete; 273   frame_allocator_restore(frame_allocator_restore const&) = delete;
274   frame_allocator_restore& 274   frame_allocator_restore&
275   operator=(frame_allocator_restore const&) = delete; 275   operator=(frame_allocator_restore const&) = delete;
276   276  
HITCBC 277   11654 ~frame_allocator_restore() 277   11540 ~frame_allocator_restore()
278   { 278   {
HITCBC 279   11654 capy::set_current_frame_allocator(saved); 279   11540 capy::set_current_frame_allocator(saved);
HITCBC 280   11654 } 280   11540 }
281   }; 281   };
282   282  
283   public: 283   public:
284   /** Dispatches and posts work to this context; see the 284   /** Dispatches and posts work to this context; see the
285   executor_type definition below. */ 285   executor_type definition below. */
286   class executor_type; 286   class executor_type;
287   287  
288   /** Construct with default concurrency and platform backend. 288   /** Construct with default concurrency and platform backend.
289   289  
290   Uses `std::thread::hardware_concurrency()` (floored to 1, in 290   Uses `std::thread::hardware_concurrency()` (floored to 1, in
291   case it reports 0) as the concurrency hint, and the default 291   case it reports 0) as the concurrency hint, and the default
292   @ref locking_mode::safe tier. Select a lockless tier via 292   @ref locking_mode::safe tier. Select a lockless tier via
293   @ref io_context_options::locking. 293   @ref io_context_options::locking.
294   294  
295   @throws std::system_error If the backend's infrastructure 295   @throws std::system_error If the backend's infrastructure
296   could not be created. 296   could not be created.
297   */ 297   */
298   io_context(); 298   io_context();
299   299  
300   /** Construct with a concurrency hint and platform backend. 300   /** Construct with a concurrency hint and platform backend.
301   301  
302   @param concurrency_hint Hint for the number of threads 302   @param concurrency_hint Hint for the number of threads
303   that calls `run()`. 303   that calls `run()`.
304   304  
305   @throws std::system_error If the backend's infrastructure 305   @throws std::system_error If the backend's infrastructure
306   could not be created. 306   could not be created.
307   */ 307   */
308   explicit io_context(unsigned concurrency_hint); 308   explicit io_context(unsigned concurrency_hint);
309   309  
310   /** Construct with runtime tuning options and platform backend. 310   /** Construct with runtime tuning options and platform backend.
311   311  
312   @param opts Runtime options controlling scheduler and 312   @param opts Runtime options controlling scheduler and
313   service behavior. 313   service behavior.
314   @param concurrency_hint Hint for the number of threads 314   @param concurrency_hint Hint for the number of threads
315   that calls `run()`. 315   that calls `run()`.
316   316  
317   @throws std::invalid_argument If `opts.thread_pool_size` is 317   @throws std::invalid_argument If `opts.thread_pool_size` is
318   less than 1 (POSIX). 318   less than 1 (POSIX).
319   319  
320   @throws std::system_error If the backend's infrastructure 320   @throws std::system_error If the backend's infrastructure
321   could not be created. 321   could not be created.
322   */ 322   */
323   explicit io_context( 323   explicit io_context(
324   io_context_options const& opts, 324   io_context_options const& opts,
325   unsigned concurrency_hint = std::thread::hardware_concurrency()); 325   unsigned concurrency_hint = std::thread::hardware_concurrency());
326   326  
327   /** Construct with an explicit backend tag. 327   /** Construct with an explicit backend tag.
328   328  
329   @tparam Backend A backend tag type that provides a static 329   @tparam Backend A backend tag type that provides a static
330   `construct(capy::execution_context&, unsigned)` factory 330   `construct(capy::execution_context&, unsigned)` factory
331   used to build the scheduler. 331   used to build the scheduler.
332   332  
333   @param backend The backend tag value selecting the I/O 333   @param backend The backend tag value selecting the I/O
334   multiplexer (e.g. `corosio::epoll`). 334   multiplexer (e.g. `corosio::epoll`).
335   @param concurrency_hint Hint for the number of threads 335   @param concurrency_hint Hint for the number of threads
336   that calls `run()`. 336   that calls `run()`.
337   337  
338   @throws std::system_error If the backend's infrastructure 338   @throws std::system_error If the backend's infrastructure
339   could not be created. 339   could not be created.
340   */ 340   */
341   template<class Backend> 341   template<class Backend>
342   requires requires { Backend::construct; } 342   requires requires { Backend::construct; }
HITCBC 343   3843 explicit io_context( 343   3858 explicit io_context(
344   [[maybe_unused]] Backend backend, 344   [[maybe_unused]] Backend backend,
345   unsigned concurrency_hint = std::thread::hardware_concurrency()) 345   unsigned concurrency_hint = std::thread::hardware_concurrency())
346   : capy::execution_context(this) 346   : capy::execution_context(this)
HITCBC 347   3843 , sched_(nullptr) 347   3858 , sched_(nullptr)
348   { 348   {
HITCBC 349   3843 sched_ = &Backend::construct(*this, concurrency_hint); 349   3858 sched_ = &Backend::construct(*this, concurrency_hint);
350   // Apply threading config only (locking tier). Unlike the options 350   // Apply threading config only (locking tier). Unlike the options
351   // ctor, the plain path leaves the reactor budget at its defaults. 351   // ctor, the plain path leaves the reactor budget at its defaults.
HITCBC 352   3831 apply_threading_(io_context_options{}); 352   3842 apply_threading_(io_context_options{});
HITCBC 353   3843 } 353   3858 }
354   354  
355   /** Construct with an explicit backend tag and runtime options. 355   /** Construct with an explicit backend tag and runtime options.
356   356  
357   @tparam Backend A backend tag type that provides a static 357   @tparam Backend A backend tag type that provides a static
358   `construct(capy::execution_context&, unsigned)` factory 358   `construct(capy::execution_context&, unsigned)` factory
359   used to build the scheduler. 359   used to build the scheduler.
360   360  
361   @param backend The backend tag value selecting the I/O 361   @param backend The backend tag value selecting the I/O
362   multiplexer (e.g. `corosio::epoll`). 362   multiplexer (e.g. `corosio::epoll`).
363   @param opts Runtime options controlling scheduler and 363   @param opts Runtime options controlling scheduler and
364   service behavior. 364   service behavior.
365   @param concurrency_hint Hint for the number of threads 365   @param concurrency_hint Hint for the number of threads
366   that calls `run()`. 366   that calls `run()`.
367   367  
368   @throws std::invalid_argument If `opts.thread_pool_size` is 368   @throws std::invalid_argument If `opts.thread_pool_size` is
369   less than 1 (POSIX). 369   less than 1 (POSIX).
370   370  
371   @throws std::system_error If the backend's infrastructure 371   @throws std::system_error If the backend's infrastructure
372   could not be created. 372   could not be created.
373   */ 373   */
374   template<class Backend> 374   template<class Backend>
375   requires requires { Backend::construct; } 375   requires requires { Backend::construct; }
HITCBC 376   45 explicit io_context( 376   45 explicit io_context(
377   [[maybe_unused]] Backend backend, 377   [[maybe_unused]] Backend backend,
378   io_context_options const& opts, 378   io_context_options const& opts,
379   unsigned concurrency_hint = std::thread::hardware_concurrency()) 379   unsigned concurrency_hint = std::thread::hardware_concurrency())
380   : capy::execution_context(this) 380   : capy::execution_context(this)
HITCBC 381   45 , sched_(nullptr) 381   45 , sched_(nullptr)
382   { 382   {
HITCBC 383   45 apply_options_pre_(opts); 383   45 apply_options_pre_(opts);
384   // Effective hint (1 for lockless tiers); see effective_concurrency_hint. 384   // Effective hint (1 for lockless tiers); see effective_concurrency_hint.
385   unsigned const eff = 385   unsigned const eff =
HITCBC 386   45 detail::effective_concurrency_hint(opts, concurrency_hint); 386   45 detail::effective_concurrency_hint(opts, concurrency_hint);
HITCBC 387   45 sched_ = &Backend::construct(*this, eff); 387   45 sched_ = &Backend::construct(*this, eff);
HITCBC 388   45 apply_options_post_(opts, eff); 388   45 apply_options_post_(opts, eff);
HITCBC 389   45 } 389   45 }
390   390  
391   /** Destroy the context, its services and the coroutines it holds. 391   /** Destroy the context, its services and the coroutines it holds.
392   392  
393   The destructor stops the loop and destroys every service. Each 393   The destructor stops the loop and destroys every service. Each
394   coroutine suspended in an operation of this context, or posted to 394   coroutine suspended in an operation of this context, or posted to
395   it and not yet resumed, is abandoned. Its chain is unwound from 395   it and not yet resumed, is abandoned. Its chain is unwound from
396   the top, on the executor of the chain's owner (see 396   the top, on the executor of the chain's owner (see
397   `capy::abandon`). The precondition below makes that executor this 397   `capy::abandon`). The precondition below makes that executor this
398   context's, so the unwind runs during this destruction. 398   context's, so the unwind runs during this destruction.
399   399  
400   @pre No thread is executing a run variant. 400   @pre No thread is executing a run variant.
401   401  
402   @pre No work crosses into another execution context. A chain 402   @pre No work crosses into another execution context. A chain
403   held here must be owned on this context. No frame of a chain 403   held here must be owned on this context. No frame of a chain
404   owned elsewhere may own I/O objects of this context, or wait 404   owned elsewhere may own I/O objects of this context, or wait
405   on a primitive that a chain held here owns. Such a chain's 405   on a primitive that a chain held here owns. Such a chain's
406   unwind would run on the other context after this one is gone, 406   unwind would run on the other context after this one is gone,
407   whatever the order of destruction. Stop such work and run both 407   whatever the order of destruction. Stop such work and run both
408   contexts until it ends before destroying either. 408   contexts until it ends before destroying either.
409   409  
410   @note Every service gives up its work while the services shut 410   @note Every service gives up its work while the services shut
411   down, so nothing is left when they are destroyed. Work posted 411   down, so nothing is left when they are destroyed. Work posted
412   after that anyway, for example from a service's destructor, is 412   after that anyway, for example from a service's destructor, is
413   abandoned while the services are being destroyed. Its 413   abandoned while the services are being destroyed. Its
414   destructors must not use any service. 414   destructors must not use any service.
415   */ 415   */
416   ~io_context(); 416   ~io_context();
417   417  
418   /// Copy construction is disabled; the context owns its services. 418   /// Copy construction is disabled; the context owns its services.
419   io_context(io_context const&) = delete; 419   io_context(io_context const&) = delete;
420   /// Copy assignment is disabled; the context owns its services. 420   /// Copy assignment is disabled; the context owns its services.
421   io_context& operator=(io_context const&) = delete; 421   io_context& operator=(io_context const&) = delete;
422   422  
423   /** Return an executor for this context. 423   /** Return an executor for this context.
424   424  
425   The returned executor can be used to dispatch coroutines 425   The returned executor can be used to dispatch coroutines
426   and post work items to this context. 426   and post work items to this context.
427   427  
428   @return An executor associated with this context. 428   @return An executor associated with this context.
429   */ 429   */
430   executor_type get_executor() const noexcept; 430   executor_type get_executor() const noexcept;
431   431  
432   /** Signal the context to stop processing. 432   /** Signal the context to stop processing.
433   433  
434   This causes `run()` to return as soon as possible. Any pending 434   This causes `run()` to return as soon as possible. Any pending
435   work items remain queued. 435   work items remain queued.
436   */ 436   */
HITCBC 437   63 void stop() 437   63 void stop()
438   { 438   {
HITCBC 439   63 sched_->stop(); 439   63 sched_->stop();
HITCBC 440   63 } 440   63 }
441   441  
442   /** Return whether the context stopped. 442   /** Return whether the context stopped.
443   443  
444   @return `true` after a call to `stop()` with no later 444   @return `true` after a call to `stop()` with no later
445   call to `restart()`. 445   call to `restart()`.
446   */ 446   */
HITCBC 447   3504 bool stopped() const noexcept 447   3514 bool stopped() const noexcept
448   { 448   {
HITCBC 449   3504 return sched_->stopped(); 449   3514 return sched_->stopped();
450   } 450   }
451   451  
452   /** Restart the context after being stopped. 452   /** Restart the context after being stopped.
453   453  
454   This function must be called before `run()` can be called 454   This function must be called before `run()` can be called
455   again after a call to `stop()`. 455   again after a call to `stop()`.
456   */ 456   */
HITCBC 457   5831 void restart() 457   5675 void restart()
458   { 458   {
HITCBC 459   5831 sched_->restart(); 459   5675 sched_->restart();
HITCBC 460   5831 } 460   5675 }
461   461  
462   /** Process all pending work items. 462   /** Process all pending work items.
463   463  
464   This function blocks until it executes all pending work items, 464   This function blocks until it executes all pending work items,
465   or until `stop()` is called. The context is stopped 465   or until `stop()` is called. The context is stopped
466   when there is no more outstanding work. 466   when there is no more outstanding work.
467   467  
468   @note The context must be restarted with `restart()` before 468   @note The context must be restarted with `restart()` before
469   calling this function again after it returns. 469   calling this function again after it returns.
470   470  
471   @return The number of handlers executed. 471   @return The number of handlers executed.
472   */ 472   */
HITCBC 473   6481 std::size_t run() 473   6354 std::size_t run()
474   { 474   {
HITCBC 475   6481 frame_allocator_restore restore; 475   6354 frame_allocator_restore restore;
HITCBC 476   12959 return sched_->run(); 476   12705 return sched_->run();
HITCBC 477   6481 } 477   6354 }
478   478  
479   /** Process at most one pending work item. 479   /** Process at most one pending work item.
480   480  
481   This function blocks until it executes one work item 481   This function blocks until it executes one work item
482   or `stop()` is called. The context is stopped when there 482   or `stop()` is called. The context is stopped when there
483   is no more outstanding work. 483   is no more outstanding work.
484   484  
485   @note The context must be restarted with `restart()` before 485   @note The context must be restarted with `restart()` before
486   calling this function again after it returns. 486   calling this function again after it returns.
487   487  
488   @return The number of handlers executed (0 or 1). 488   @return The number of handlers executed (0 or 1).
489   */ 489   */
HITCBC 490   128 std::size_t run_one() 490   126 std::size_t run_one()
491   { 491   {
HITCBC 492   128 frame_allocator_restore restore; 492   126 frame_allocator_restore restore;
HITCBC 493   256 return sched_->run_one(); 493   252 return sched_->run_one();
HITCBC 494   128 } 494   126 }
495   495  
496   /** Process work items for the specified duration. 496   /** Process work items for the specified duration.
497   497  
498   This function blocks until it has executed work items for the 498   This function blocks until it has executed work items for the
499   specified duration, or until `stop()` is called. The context 499   specified duration, or until `stop()` is called. The context
500   is stopped when there is no more outstanding work. 500   is stopped when there is no more outstanding work.
501   501  
502   @note The context must be restarted with `restart()` before 502   @note The context must be restarted with `restart()` before
503   calling this function again after it returns. 503   calling this function again after it returns.
504   504  
505   @param rel_time The duration for which to process work. 505   @param rel_time The duration for which to process work.
506   506  
507   @return The number of handlers executed. 507   @return The number of handlers executed.
508   */ 508   */
509   template<class Rep, class Period> 509   template<class Rep, class Period>
HITCBC 510   827 std::size_t run_for(std::chrono::duration<Rep, Period> const& rel_time) 510   827 std::size_t run_for(std::chrono::duration<Rep, Period> const& rel_time)
511   { 511   {
HITCBC 512   827 return run_until(std::chrono::steady_clock::now() + rel_time); 512   827 return run_until(std::chrono::steady_clock::now() + rel_time);
513   } 513   }
514   514  
515   /** Process work items until the specified time. 515   /** Process work items until the specified time.
516   516  
517   This function blocks until the specified time is reached 517   This function blocks until the specified time is reached
518   or `stop()` is called. The context is stopped when there 518   or `stop()` is called. The context is stopped when there
519   is no more outstanding work. 519   is no more outstanding work.
520   520  
521   @note The context must be restarted with `restart()` before 521   @note The context must be restarted with `restart()` before
522   calling this function again after it returns. 522   calling this function again after it returns.
523   523  
524   @param abs_time The time point until which to process work. 524   @param abs_time The time point until which to process work.
525   525  
526   @return The number of handlers executed. 526   @return The number of handlers executed.
527   */ 527   */
528   template<class Clock, class Duration> 528   template<class Clock, class Duration>
529   std::size_t 529   std::size_t
HITCBC 530   828 run_until(std::chrono::time_point<Clock, Duration> const& abs_time) 530   828 run_until(std::chrono::time_point<Clock, Duration> const& abs_time)
531   { 531   {
HITCBC 532   828 std::size_t n = 0; 532   828 std::size_t n = 0;
HITCBC 533   2433 while (run_one_until(abs_time)) 533   2447 while (run_one_until(abs_time))
HITCBC 534   1605 if (n != (std::numeric_limits<std::size_t>::max)()) 534   1619 if (n != (std::numeric_limits<std::size_t>::max)())
HITCBC 535   1605 ++n; 535   1619 ++n;
HITCBC 536   828 return n; 536   828 return n;
537   } 537   }
538   538  
539   /** Process at most one work item for the specified duration. 539   /** Process at most one work item for the specified duration.
540   540  
541   This function blocks until it executes one work item, 541   This function blocks until it executes one work item,
542   the specified duration has elapsed, or `stop()` is called. 542   the specified duration has elapsed, or `stop()` is called.
543   The context is stopped when there is no more outstanding work. 543   The context is stopped when there is no more outstanding work.
544   544  
545   @note The context must be restarted with `restart()` before 545   @note The context must be restarted with `restart()` before
546   calling this function again after it returns. 546   calling this function again after it returns.
547   547  
548   @param rel_time The duration for which the call may block. 548   @param rel_time The duration for which the call may block.
549   549  
550   @return The number of handlers executed (0 or 1). 550   @return The number of handlers executed (0 or 1).
551   */ 551   */
552   template<class Rep, class Period> 552   template<class Rep, class Period>
HITCBC 553   1072 std::size_t run_one_for(std::chrono::duration<Rep, Period> const& rel_time) 553   1069 std::size_t run_one_for(std::chrono::duration<Rep, Period> const& rel_time)
554   { 554   {
HITCBC 555   1072 return run_one_until(std::chrono::steady_clock::now() + rel_time); 555   1069 return run_one_until(std::chrono::steady_clock::now() + rel_time);
556   } 556   }
557   557  
558   /** Process at most one work item until the specified time. 558   /** Process at most one work item until the specified time.
559   559  
560   This function blocks until it executes one work item, 560   This function blocks until it executes one work item,
561   the specified time is reached, or `stop()` is called. 561   the specified time is reached, or `stop()` is called.
562   The context is stopped when there is no more outstanding work. 562   The context is stopped when there is no more outstanding work.
563   563  
564   @note The context must be restarted with `restart()` before 564   @note The context must be restarted with `restart()` before
565   calling this function again after it returns. 565   calling this function again after it returns.
566   566  
567   @param abs_time The time point until which the call may block. 567   @param abs_time The time point until which the call may block.
568   568  
569   @return The number of handlers executed (0 or 1). 569   @return The number of handlers executed (0 or 1).
570   */ 570   */
571   template<class Clock, class Duration> 571   template<class Clock, class Duration>
572   std::size_t 572   std::size_t
HITCBC 573   3513 run_one_until(std::chrono::time_point<Clock, Duration> const& abs_time) 573   3524 run_one_until(std::chrono::time_point<Clock, Duration> const& abs_time)
574   { 574   {
HITCBC 575   3513 frame_allocator_restore restore; 575   3524 frame_allocator_restore restore;
HITCBC 576   3513 typename Clock::time_point now = Clock::now(); 576   3524 typename Clock::time_point now = Clock::now();
HITCBC 577   2580 for (;;) 577   2590 for (;;)
578   { 578   {
HITCBC 579   6093 auto rel_time = abs_time - now; 579   6114 auto rel_time = abs_time - now;
580   using rel_type = decltype(rel_time); 580   using rel_type = decltype(rel_time);
HITCBC 581   6093 if (rel_time < rel_type::zero()) 581   6114 if (rel_time < rel_type::zero())
HITCBC 582   5 rel_time = rel_type::zero(); 582   5 rel_time = rel_type::zero();
HITCBC 583   6088 else if (rel_time > std::chrono::seconds(1)) 583   6109 else if (rel_time > std::chrono::seconds(1))
HITCBC 584   5899 rel_time = std::chrono::seconds(1); 584   5921 rel_time = std::chrono::seconds(1);
585   585  
HITCBC 586   6093 std::size_t s = sched_->wait_one( 586   6114 std::size_t s = sched_->wait_one(
587   static_cast<long>( 587   static_cast<long>(
HITCBC 588   6093 std::chrono::duration_cast<std::chrono::microseconds>( 588   6114 std::chrono::duration_cast<std::chrono::microseconds>(
589   rel_time) 589   rel_time)
HITCBC 590   6093 .count())); 590   6114 .count()));
591   591  
HITCBC 592   6093 if (s || stopped()) 592   6114 if (s || stopped())
HITCBC 593   3477 return s; 593   3488 return s;
594   594  
HITCBC 595   2616 now = Clock::now(); 595   2626 now = Clock::now();
HITCBC 596   2616 if (now >= abs_time) 596   2626 if (now >= abs_time)
HITCBC 597   36 return 0; 597   36 return 0;
598   } 598   }
HITCBC 599   3513 } 599   3524 }
600   600  
601   /** Process all ready work items without blocking. 601   /** Process all ready work items without blocking.
602   602  
603   This function executes all work items that are ready to run 603   This function executes all work items that are ready to run
604   without blocking for more work. The context is stopped 604   without blocking for more work. The context is stopped
605   when there is no more outstanding work. 605   when there is no more outstanding work.
606   606  
607   @note The context must be restarted with `restart()` before 607   @note The context must be restarted with `restart()` before
608   calling this function again after it returns. 608   calling this function again after it returns.
609   609  
610   @return The number of handlers executed. 610   @return The number of handlers executed.
611   */ 611   */
HITCBC 612   1325 std::size_t poll() 612   1327 std::size_t poll()
613   { 613   {
HITCBC 614   1325 frame_allocator_restore restore; 614   1327 frame_allocator_restore restore;
HITCBC 615   2650 return sched_->poll(); 615   2654 return sched_->poll();
HITCBC 616   1325 } 616   1327 }
617   617  
618   /** Process at most one ready work item without blocking. 618   /** Process at most one ready work item without blocking.
619   619  
620   This function executes at most one work item that is ready 620   This function executes at most one work item that is ready
621   to run without blocking for more work. The context is 621   to run without blocking for more work. The context is
622   stopped when there is no more outstanding work. 622   stopped when there is no more outstanding work.
623   623  
624   @note The context must be restarted with `restart()` before 624   @note The context must be restarted with `restart()` before
625   calling this function again after it returns. 625   calling this function again after it returns.
626   626  
627   @return The number of handlers executed (0 or 1). 627   @return The number of handlers executed (0 or 1).
628   */ 628   */
HITCBC 629   56 std::size_t poll_one() 629   58 std::size_t poll_one()
630   { 630   {
HITCBC 631   56 frame_allocator_restore restore; 631   58 frame_allocator_restore restore;
HITCBC 632   112 return sched_->poll_one(); 632   116 return sched_->poll_one();
HITCBC 633   56 } 633   58 }
634   }; 634   };
635   635  
636   /** Dispatches and posts work to an I/O context. 636   /** Dispatches and posts work to an I/O context.
637   637  
638   The executor provides the interface for posting work items and 638   The executor provides the interface for posting work items and
639   dispatching coroutines to the associated context. It satisfies 639   dispatching coroutines to the associated context. It satisfies
640   the `capy::Executor` concept. 640   the `capy::Executor` concept.
641   641  
642   Executors are lightweight handles that can be copied and compared 642   Executors are lightweight handles that can be copied and compared
643   for equality. Two executors compare equal if they refer to the 643   for equality. Two executors compare equal if they refer to the
644   same context. 644   same context.
645   645  
646   @par Thread Safety 646   @par Thread Safety
647   Distinct objects: Safe.@n 647   Distinct objects: Safe.@n
648   Shared objects: Safe. 648   Shared objects: Safe.
649   */ 649   */
650   class io_context::executor_type 650   class io_context::executor_type
651   { 651   {
652   io_context* ctx_ = nullptr; 652   io_context* ctx_ = nullptr;
653   653  
654   public: 654   public:
655   /** Constructs an executor not associated with any context. */ 655   /** Constructs an executor not associated with any context. */
HITCBC 656   2057 executor_type() = default; 656   2057 executor_type() = default;
657   657  
658   /** Construct an executor from a context. 658   /** Construct an executor from a context.
659   659  
660   @param ctx The context to associate with this executor. 660   @param ctx The context to associate with this executor.
661   */ 661   */
HITCBC 662   18334 explicit executor_type(io_context& ctx) noexcept : ctx_(&ctx) {} 662   18293 explicit executor_type(io_context& ctx) noexcept : ctx_(&ctx) {}
663   663  
664   /** Return a reference to the associated execution context. 664   /** Return a reference to the associated execution context.
665   665  
666   @return Reference to the context. 666   @return Reference to the context.
667   */ 667   */
HITCBC 668   46604 io_context& context() const noexcept 668   46137 io_context& context() const noexcept
669   { 669   {
HITCBC 670   46604 return *ctx_; 670   46137 return *ctx_;
671   } 671   }
672   672  
673   /** Check if the current thread is running this executor's context. 673   /** Check if the current thread is running this executor's context.
674   674  
675   @return `true` if `run()` is being called on this thread. 675   @return `true` if `run()` is being called on this thread.
676   */ 676   */
HITCBC 677   495028 bool running_in_this_thread() const noexcept 677   499930 bool running_in_this_thread() const noexcept
678   { 678   {
679   // The innermost scheduler answers the common case without a 679   // The innermost scheduler answers the common case without a
680   // virtual call or a frame walk. 680   // virtual call or a frame walk.
HITCBC 681   518875 return detail::running_scheduler.get() == ctx_->sched_ || 681   523650 return detail::running_scheduler.get() == ctx_->sched_ ||
HITCBC 682   518875 ctx_->sched_->running_in_this_thread(); 682   523650 ctx_->sched_->running_in_this_thread();
683   } 683   }
684   684  
685   /** Informs the executor that work is beginning. 685   /** Informs the executor that work is beginning.
686   686  
687   Must be paired with `on_work_finished()`. 687   Must be paired with `on_work_finished()`.
688   */ 688   */
HITCBC 689   25186 void on_work_started() const noexcept 689   25158 void on_work_started() const noexcept
690   { 690   {
HITCBC 691   25186 ctx_->sched_->work_started(); 691   25158 ctx_->sched_->work_started();
HITCBC 692   25186 } 692   25158 }
693   693  
694   /** Informs the executor that work has completed. 694   /** Informs the executor that work has completed.
695   695  
696   @pre A preceding call to `on_work_started()` on an equal executor. 696   @pre A preceding call to `on_work_started()` on an equal executor.
697   */ 697   */
HITCBC 698   25186 void on_work_finished() const noexcept 698   25158 void on_work_finished() const noexcept
699   { 699   {
HITCBC 700   25186 ctx_->sched_->work_finished(); 700   25158 ctx_->sched_->work_finished();
HITCBC 701   25186 } 701   25158 }
702   702  
703   /** Dispatch a continuation. 703   /** Dispatch a continuation.
704   704  
705   Returns a handle for symmetric transfer. If called from 705   Returns a handle for symmetric transfer. If called from
706   within `run()`, returns `c.h`. Otherwise posts `c` for 706   within `run()`, returns `c.h`. Otherwise posts `c` for
707   later execution and returns `std::noop_coroutine()`. 707   later execution and returns `std::noop_coroutine()`.
708   708  
709   @param c The continuation to dispatch. 709   @param c The continuation to dispatch.
710   710  
711   @return A handle for symmetric transfer or `std::noop_coroutine()`. 711   @return A handle for symmetric transfer or `std::noop_coroutine()`.
712   712  
713   @pre The associated context must outlive this call. Dispatching 713   @pre The associated context must outlive this call. Dispatching
714   concurrently with, or after, the context's destruction is 714   concurrently with, or after, the context's destruction is
715   undefined behavior. 715   undefined behavior.
716   */ 716   */
HITCBC 717   495021 std::coroutine_handle<> dispatch(capy::continuation& c) const 717   499923 std::coroutine_handle<> dispatch(capy::continuation& c) const
718   { 718   {
HITCBC 719   495021 if (running_in_this_thread()) 719   499923 if (running_in_this_thread())
HITCBC 720   471177 return c.h; 720   476206 return c.h;
HITCBC 721   23844 post(c); 721   23717 post(c);
HITCBC 722   23844 return std::noop_coroutine(); 722   23717 return std::noop_coroutine();
723   } 723   }
724   724  
725   /** Post a continuation for deferred execution. 725   /** Post a continuation for deferred execution.
726   726  
727   Enqueues `c` directly on the scheduler's ready queue. 727   Enqueues `c` directly on the scheduler's ready queue.
728   No heap allocation occurs. 728   No heap allocation occurs.
729   729  
730   @param c The continuation to enqueue. 730   @param c The continuation to enqueue.
731   731  
732   @pre The associated context must outlive this call. Posting 732   @pre The associated context must outlive this call. Posting
733   concurrently with, or after, the context's destruction is 733   concurrently with, or after, the context's destruction is
734   undefined behavior. 734   undefined behavior.
735   */ 735   */
HITCBC 736   252644 void post(capy::continuation& c) const 736   236425 void post(capy::continuation& c) const
737   { 737   {
HITCBC 738   252644 ctx_->sched_->post(c); 738   236425 ctx_->sched_->post(c);
HITCBC 739   252644 } 739   236425 }
740   740  
741   /** Post a bare coroutine handle for deferred execution. 741   /** Post a bare coroutine handle for deferred execution.
742   742  
743   Heap-allocates a `scheduler_op` to wrap the handle. A caller 743   Heap-allocates a `scheduler_op` to wrap the handle. A caller
744   that already owns a `capy::continuation` can post it directly 744   that already owns a `capy::continuation` can post it directly
745   via the `post(capy::continuation&)` overload to avoid the 745   via the `post(capy::continuation&)` overload to avoid the
746   allocation. 746   allocation.
747   747  
748   A handle still queued when the context is torn down is 748   A handle still queued when the context is torn down is
749   destroyed alone, since it carries no owner to unwind its chain 749   destroyed alone, since it carries no owner to unwind its chain
750   from. Post a coroutine that another frame awaits through the 750   from. Post a coroutine that another frame awaits through the
751   `post(capy::continuation&)` overload, with its owner set. 751   `post(capy::continuation&)` overload, with its owner set.
752   752  
753   @param h The coroutine handle to post. 753   @param h The coroutine handle to post.
754   754  
755   @pre The associated context must outlive this call. Posting 755   @pre The associated context must outlive this call. Posting
756   concurrently with, or after, the context's destruction is 756   concurrently with, or after, the context's destruction is
757   undefined behavior. 757   undefined behavior.
758   758  
759   @pre Nothing awaits `h`. 759   @pre Nothing awaits `h`.
760   */ 760   */
HITCBC 761   3672 void post(std::coroutine_handle<> h) const 761   3672 void post(std::coroutine_handle<> h) const
762   { 762   {
HITCBC 763   3672 ctx_->sched_->post(h); 763   3672 ctx_->sched_->post(h);
HITCBC 764   3672 } 764   3672 }
765   765  
766   /** Compare two executors for equality. 766   /** Compare two executors for equality.
767   767  
768   @return `true` if both executors refer to the same context. 768   @return `true` if both executors refer to the same context.
769   */ 769   */
HITCBC 770   2 bool operator==(executor_type const& other) const noexcept 770   2 bool operator==(executor_type const& other) const noexcept
771   { 771   {
HITCBC 772   2 return ctx_ == other.ctx_; 772   2 return ctx_ == other.ctx_;
773   } 773   }
774   774  
775   /** Compare two executors for inequality. 775   /** Compare two executors for inequality.
776   776  
777   @return `true` if the executors refer to different contexts. 777   @return `true` if the executors refer to different contexts.
778   */ 778   */
779   bool operator!=(executor_type const& other) const noexcept 779   bool operator!=(executor_type const& other) const noexcept
780   { 780   {
781   return ctx_ != other.ctx_; 781   return ctx_ != other.ctx_;
782   } 782   }
783   }; 783   };
784   784  
785   inline io_context::executor_type 785   inline io_context::executor_type
HITCBC 786   18334 io_context::get_executor() const noexcept 786   18293 io_context::get_executor() const noexcept
787   { 787   {
HITCBC 788   18334 return executor_type(const_cast<io_context&>(*this)); 788   18293 return executor_type(const_cast<io_context&>(*this));
789   } 789   }
790   790  
791   } // namespace boost::corosio 791   } // namespace boost::corosio
792   792  
793   #endif // BOOST_COROSIO_IO_CONTEXT_HPP 793   #endif // BOOST_COROSIO_IO_CONTEXT_HPP