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