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