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/continuation_op.hpp>
17 : #include <boost/corosio/detail/platform.hpp>
18 : #include <boost/corosio/detail/scheduler.hpp>
19 : #include <boost/capy/continuation.hpp>
20 : #include <boost/capy/ex/execution_context.hpp>
21 :
22 : #include <chrono>
23 : #include <coroutine>
24 : #include <cstddef>
25 : #include <limits>
26 : #include <thread>
27 :
28 : namespace boost::corosio {
29 :
30 : /** Runtime tuning options for @ref io_context.
31 :
32 : All fields have defaults that match the library's built-in
33 : values, so constructing a default `io_context_options` produces
34 : identical behavior to an unconfigured context.
35 :
36 : Options that apply only to a specific backend family are
37 : silently ignored when the active backend does not support them.
38 :
39 : @par Example
40 : @code
41 : io_context_options opts;
42 : opts.max_events_per_poll = 256; // larger batch per syscall
43 : opts.inline_budget_max = 32; // more speculative completions
44 : opts.thread_pool_size = 4; // more file-I/O workers
45 :
46 : io_context ioc(opts);
47 : @endcode
48 :
49 : @see io_context, native_io_context
50 : */
51 : struct io_context_options
52 : {
53 : /** Maximum events fetched per reactor poll call.
54 :
55 : Controls the buffer size passed to `epoll_wait()` or
56 : `kevent()`. Larger values reduce syscall frequency under
57 : high load; smaller values improve fairness between
58 : connections. Ignored on IOCP and select backends.
59 : */
60 : unsigned max_events_per_poll = 128;
61 :
62 : /** Starting inline completion budget per handler chain.
63 :
64 : After a posted handler executes, the reactor grants this
65 : many speculative inline completions before forcing a
66 : re-queue. Applies to reactor backends only.
67 :
68 : @note Constructing an `io_context` with `concurrency_hint > 1`
69 : and all three budget fields at their defaults overrides
70 : them to disable inline completion (post-everything mode),
71 : since multi-thread workloads benefit from cross-thread
72 : work-stealing. Setting any budget field to a non-default
73 : value disables the override.
74 : */
75 : unsigned inline_budget_initial = 2;
76 :
77 : /** Hard ceiling on adaptive inline budget ramp-up.
78 :
79 : The budget doubles each cycle it is fully consumed, up to
80 : this limit. Applies to reactor backends only.
81 : */
82 : unsigned inline_budget_max = 16;
83 :
84 : /** Inline budget when no other thread assists the reactor.
85 :
86 : When only one thread is running the event loop, this
87 : value caps the inline budget to preserve fairness.
88 : Applies to reactor backends only.
89 : */
90 : unsigned unassisted_budget = 4;
91 :
92 : /** Maximum `GetQueuedCompletionStatus` timeout in milliseconds.
93 :
94 : Bounds how long the IOCP scheduler blocks between timer
95 : rechecks. Lower values improve timer responsiveness at the
96 : cost of more syscalls. Applies to IOCP only.
97 : */
98 : unsigned gqcs_timeout_ms = 500;
99 :
100 : /** Thread pool size for blocking I/O (file I/O, DNS resolution).
101 :
102 : Sets the number of worker threads in the shared thread pool
103 : used by POSIX file services and DNS resolution. Must be at
104 : least 1. Applies to POSIX backends only; ignored on IOCP
105 : where file I/O uses native overlapped I/O.
106 : */
107 : unsigned thread_pool_size = 1;
108 :
109 : /** Enable single-threaded mode (disable scheduler locking).
110 :
111 : When true, the scheduler skips all mutex lock/unlock and
112 : condition variable operations on the hot path. This
113 : eliminates synchronization overhead when only one thread
114 : calls `run()`.
115 :
116 : @par Restrictions
117 : - Only one thread may call `run()` (or any run variant).
118 : - Posting work from another thread is undefined behavior.
119 : - DNS resolution returns `operation_not_supported`.
120 : - POSIX file I/O returns `operation_not_supported`.
121 : - Signal sets should not be shared across contexts.
122 :
123 : @note Constructing an `io_context` with `concurrency_hint == 1`
124 : automatically enables single-threaded mode regardless of
125 : this field's value, matching asio's convention. To opt out,
126 : pass `concurrency_hint > 1`.
127 : */
128 : bool single_threaded = false;
129 : };
130 :
131 : namespace detail {
132 : class timer_service;
133 : struct timer_service_access;
134 : } // namespace detail
135 :
136 : /** An I/O context for running asynchronous operations.
137 :
138 : The io_context provides an execution environment for async
139 : operations. It maintains a queue of pending work items and
140 : processes them when `run()` is called.
141 :
142 : The default and unsigned constructors select the platform's
143 : native backend:
144 : - Windows: IOCP
145 : - Linux: epoll
146 : - BSD/macOS: kqueue
147 : - Other POSIX: select
148 :
149 : The template constructor accepts a backend tag value to
150 : choose a specific backend at compile time:
151 :
152 : @par Example
153 : @code
154 : io_context ioc; // platform default
155 : io_context ioc2(corosio::epoll); // explicit backend
156 : @endcode
157 :
158 : @par Thread Safety
159 : Distinct objects: Safe.@n
160 : Shared objects: Safe, if using a concurrency hint greater
161 : than 1.
162 :
163 : @see epoll_t, select_t, kqueue_t, iocp_t
164 : */
165 : class BOOST_COROSIO_DECL io_context : public capy::execution_context
166 : {
167 : friend struct detail::timer_service_access;
168 :
169 : /// Pre-create services that depend on options (before construct).
170 : void apply_options_pre_(io_context_options const& opts);
171 :
172 : /// Apply runtime tuning to the scheduler (after construct).
173 : void apply_options_post_(
174 : io_context_options const& opts,
175 : unsigned concurrency_hint);
176 :
177 : /// Switch the scheduler to single-threaded (lockless) mode.
178 : void configure_single_threaded_();
179 :
180 : protected:
181 : detail::timer_service* timer_svc_ = nullptr;
182 : detail::scheduler* sched_;
183 :
184 : public:
185 : /** The executor type for this context. */
186 : class executor_type;
187 :
188 : /** Construct with default concurrency and platform backend.
189 :
190 : Uses `std::thread::hardware_concurrency()` clamped to a minimum
191 : of 2 as the concurrency hint, so the default constructor never
192 : silently engages single-threaded mode (see
193 : @ref io_context_options::single_threaded). Pass an explicit
194 : `concurrency_hint == 1` to opt into single-threaded mode.
195 : */
196 : io_context();
197 :
198 : /** Construct with a concurrency hint and platform backend.
199 :
200 : @param concurrency_hint Hint for the number of threads
201 : that will call `run()`.
202 : */
203 : explicit io_context(unsigned concurrency_hint);
204 :
205 : /** Construct with runtime tuning options and platform backend.
206 :
207 : @param opts Runtime options controlling scheduler and
208 : service behavior.
209 : @param concurrency_hint Hint for the number of threads
210 : that will call `run()`.
211 : */
212 : explicit io_context(
213 : io_context_options const& opts,
214 : unsigned concurrency_hint = std::thread::hardware_concurrency());
215 :
216 : /** Construct with an explicit backend tag.
217 :
218 : @param backend The backend tag value selecting the I/O
219 : multiplexer (e.g. `corosio::epoll`).
220 : @param concurrency_hint Hint for the number of threads
221 : that will call `run()`.
222 : */
223 : template<class Backend>
224 : requires requires { Backend::construct; }
225 HIT 848 : explicit io_context(
226 : Backend backend,
227 : unsigned concurrency_hint = std::thread::hardware_concurrency())
228 : : capy::execution_context(this)
229 848 : , sched_(nullptr)
230 : {
231 : (void)backend;
232 848 : sched_ = &Backend::construct(*this, concurrency_hint);
233 848 : if (concurrency_hint == 1)
234 2 : configure_single_threaded_();
235 848 : }
236 :
237 : /** Construct with an explicit backend tag and runtime options.
238 :
239 : @param backend The backend tag value selecting the I/O
240 : multiplexer (e.g. `corosio::epoll`).
241 : @param opts Runtime options controlling scheduler and
242 : service behavior.
243 : @param concurrency_hint Hint for the number of threads
244 : that will call `run()`.
245 : */
246 : template<class Backend>
247 : requires requires { Backend::construct; }
248 8 : explicit io_context(
249 : Backend backend,
250 : io_context_options const& opts,
251 : unsigned concurrency_hint = std::thread::hardware_concurrency())
252 : : capy::execution_context(this)
253 8 : , sched_(nullptr)
254 : {
255 : (void)backend;
256 8 : apply_options_pre_(opts);
257 8 : sched_ = &Backend::construct(*this, concurrency_hint);
258 8 : apply_options_post_(opts, concurrency_hint);
259 8 : }
260 :
261 : ~io_context();
262 :
263 : io_context(io_context const&) = delete;
264 : io_context& operator=(io_context const&) = delete;
265 :
266 : /** Return an executor for this context.
267 :
268 : The returned executor can be used to dispatch coroutines
269 : and post work items to this context.
270 :
271 : @return An executor associated with this context.
272 : */
273 : executor_type get_executor() const noexcept;
274 :
275 : /** Signal the context to stop processing.
276 :
277 : This causes `run()` to return as soon as possible. Any pending
278 : work items remain queued.
279 : */
280 5 : void stop()
281 : {
282 5 : sched_->stop();
283 5 : }
284 :
285 : /** Return whether the context has been stopped.
286 :
287 : @return `true` if `stop()` has been called and `restart()`
288 : has not been called since.
289 : */
290 34 : bool stopped() const noexcept
291 : {
292 34 : return sched_->stopped();
293 : }
294 :
295 : /** Restart the context after being stopped.
296 :
297 : This function must be called before `run()` can be called
298 : again after `stop()` has been called.
299 : */
300 156 : void restart()
301 : {
302 156 : sched_->restart();
303 156 : }
304 :
305 : /** Process all pending work items.
306 :
307 : This function blocks until all pending work items have been
308 : executed or `stop()` is called. The context is stopped
309 : when there is no more outstanding work.
310 :
311 : @note The context must be restarted with `restart()` before
312 : calling this function again after it returns.
313 :
314 : @return The number of handlers executed.
315 : */
316 667 : std::size_t run()
317 : {
318 667 : return sched_->run();
319 : }
320 :
321 : /** Process at most one pending work item.
322 :
323 : This function blocks until one work item has been executed
324 : or `stop()` is called. The context is stopped when there
325 : is no more outstanding work.
326 :
327 : @note The context must be restarted with `restart()` before
328 : calling this function again after it returns.
329 :
330 : @return The number of handlers executed (0 or 1).
331 : */
332 2 : std::size_t run_one()
333 : {
334 2 : return sched_->run_one();
335 : }
336 :
337 : /** Process work items for the specified duration.
338 :
339 : This function blocks until work items have been executed for
340 : the specified duration, or `stop()` is called. The context
341 : is stopped when there is no more outstanding work.
342 :
343 : @note The context must be restarted with `restart()` before
344 : calling this function again after it returns.
345 :
346 : @param rel_time The duration for which to process work.
347 :
348 : @return The number of handlers executed.
349 : */
350 : template<class Rep, class Period>
351 5 : std::size_t run_for(std::chrono::duration<Rep, Period> const& rel_time)
352 : {
353 5 : return run_until(std::chrono::steady_clock::now() + rel_time);
354 : }
355 :
356 : /** Process work items until the specified time.
357 :
358 : This function blocks until the specified time is reached
359 : or `stop()` is called. The context is stopped when there
360 : is no more outstanding work.
361 :
362 : @note The context must be restarted with `restart()` before
363 : calling this function again after it returns.
364 :
365 : @param abs_time The time point until which to process work.
366 :
367 : @return The number of handlers executed.
368 : */
369 : template<class Clock, class Duration>
370 : std::size_t
371 5 : run_until(std::chrono::time_point<Clock, Duration> const& abs_time)
372 : {
373 5 : std::size_t n = 0;
374 14 : while (run_one_until(abs_time))
375 9 : if (n != (std::numeric_limits<std::size_t>::max)())
376 9 : ++n;
377 5 : return n;
378 : }
379 :
380 : /** Process at most one work item for the specified duration.
381 :
382 : This function blocks until one work item has been executed,
383 : the specified duration has elapsed, or `stop()` is called.
384 : The context is stopped when there is no more outstanding work.
385 :
386 : @note The context must be restarted with `restart()` before
387 : calling this function again after it returns.
388 :
389 : @param rel_time The duration for which the call may block.
390 :
391 : @return The number of handlers executed (0 or 1).
392 : */
393 : template<class Rep, class Period>
394 3 : std::size_t run_one_for(std::chrono::duration<Rep, Period> const& rel_time)
395 : {
396 3 : return run_one_until(std::chrono::steady_clock::now() + rel_time);
397 : }
398 :
399 : /** Process at most one work item until the specified time.
400 :
401 : This function blocks until one work item has been executed,
402 : the specified time is reached, or `stop()` is called.
403 : The context is stopped when there is no more outstanding work.
404 :
405 : @note The context must be restarted with `restart()` before
406 : calling this function again after it returns.
407 :
408 : @param abs_time The time point until which the call may block.
409 :
410 : @return The number of handlers executed (0 or 1).
411 : */
412 : template<class Clock, class Duration>
413 : std::size_t
414 21 : run_one_until(std::chrono::time_point<Clock, Duration> const& abs_time)
415 : {
416 21 : typename Clock::time_point now = Clock::now();
417 4 : for (;;)
418 : {
419 25 : auto rel_time = abs_time - now;
420 : using rel_type = decltype(rel_time);
421 25 : if (rel_time < rel_type::zero())
422 2 : rel_time = rel_type::zero();
423 23 : else if (rel_time > std::chrono::seconds(1))
424 11 : rel_time = std::chrono::seconds(1);
425 :
426 25 : std::size_t s = sched_->wait_one(
427 : static_cast<long>(
428 25 : std::chrono::duration_cast<std::chrono::microseconds>(
429 : rel_time)
430 25 : .count()));
431 :
432 25 : if (s || stopped())
433 21 : return s;
434 :
435 6 : now = Clock::now();
436 6 : if (now >= abs_time)
437 2 : return 0;
438 : }
439 : }
440 :
441 : /** Process all ready work items without blocking.
442 :
443 : This function executes all work items that are ready to run
444 : without blocking for more work. The context is stopped
445 : when there is no more outstanding work.
446 :
447 : @note The context must be restarted with `restart()` before
448 : calling this function again after it returns.
449 :
450 : @return The number of handlers executed.
451 : */
452 24 : std::size_t poll()
453 : {
454 24 : return sched_->poll();
455 : }
456 :
457 : /** Process at most one ready work item without blocking.
458 :
459 : This function executes at most one work item that is ready
460 : to run without blocking for more work. The context is
461 : stopped when there is no more outstanding work.
462 :
463 : @note The context must be restarted with `restart()` before
464 : calling this function again after it returns.
465 :
466 : @return The number of handlers executed (0 or 1).
467 : */
468 4 : std::size_t poll_one()
469 : {
470 4 : return sched_->poll_one();
471 : }
472 : };
473 :
474 : /** An executor for dispatching work to an I/O context.
475 :
476 : The executor provides the interface for posting work items and
477 : dispatching coroutines to the associated context. It satisfies
478 : the `capy::Executor` concept.
479 :
480 : Executors are lightweight handles that can be copied and compared
481 : for equality. Two executors compare equal if they refer to the
482 : same context.
483 :
484 : @par Thread Safety
485 : Distinct objects: Safe.@n
486 : Shared objects: Safe.
487 : */
488 : class io_context::executor_type
489 : {
490 : io_context* ctx_ = nullptr;
491 :
492 : public:
493 : /** Default constructor.
494 :
495 : Constructs an executor not associated with any context.
496 : */
497 : executor_type() = default;
498 :
499 : /** Construct an executor from a context.
500 :
501 : @param ctx The context to associate with this executor.
502 : */
503 937 : explicit executor_type(io_context& ctx) noexcept : ctx_(&ctx) {}
504 :
505 : /** Return a reference to the associated execution context.
506 :
507 : @return Reference to the context.
508 : */
509 1768 : io_context& context() const noexcept
510 : {
511 1768 : return *ctx_;
512 : }
513 :
514 : /** Check if the current thread is running this executor's context.
515 :
516 : @return `true` if `run()` is being called on this thread.
517 : */
518 1796 : bool running_in_this_thread() const noexcept
519 : {
520 1796 : return ctx_->sched_->running_in_this_thread();
521 : }
522 :
523 : /** Informs the executor that work is beginning.
524 :
525 : Must be paired with `on_work_finished()`.
526 : */
527 1960 : void on_work_started() const noexcept
528 : {
529 1960 : ctx_->sched_->work_started();
530 1960 : }
531 :
532 : /** Informs the executor that work has completed.
533 :
534 : @par Preconditions
535 : A preceding call to `on_work_started()` on an equal executor.
536 : */
537 1927 : void on_work_finished() const noexcept
538 : {
539 1927 : ctx_->sched_->work_finished();
540 1927 : }
541 :
542 : /** Dispatch a continuation.
543 :
544 : Returns a handle for symmetric transfer. If called from
545 : within `run()`, returns `c.h`. Otherwise posts the
546 : enclosing continuation_op as a scheduler_op for later
547 : execution and returns `std::noop_coroutine()`.
548 :
549 : @param c The continuation to dispatch. Must be the `cont`
550 : member of a `detail::continuation_op`.
551 :
552 : @return A handle for symmetric transfer or `std::noop_coroutine()`.
553 : */
554 1794 : std::coroutine_handle<> dispatch(capy::continuation& c) const
555 : {
556 1794 : if (running_in_this_thread())
557 653 : return c.h;
558 1141 : post(c);
559 1141 : return std::noop_coroutine();
560 : }
561 :
562 : /** Post a continuation for deferred execution.
563 :
564 : If the continuation is backed by a continuation_op
565 : (tagged), posts it directly as a scheduler_op — zero
566 : heap allocation. Otherwise falls back to the
567 : heap-allocating post(coroutine_handle<>) path.
568 : */
569 11096 : void post(capy::continuation& c) const
570 : {
571 11096 : auto* op = detail::continuation_op::try_from_continuation(c);
572 11096 : if (op)
573 9952 : ctx_->sched_->post(op);
574 : else
575 1144 : ctx_->sched_->post(c.h);
576 11096 : }
577 :
578 : /** Post a bare coroutine handle for deferred execution.
579 :
580 : Heap-allocates a scheduler_op to wrap the handle. Prefer
581 : posting through a continuation_op-backed continuation when
582 : the continuation has suitable lifetime.
583 :
584 : @param h The coroutine handle to post.
585 : */
586 1441 : void post(std::coroutine_handle<> h) const
587 : {
588 1441 : ctx_->sched_->post(h);
589 1441 : }
590 :
591 : /** Compare two executors for equality.
592 :
593 : @return `true` if both executors refer to the same context.
594 : */
595 1 : bool operator==(executor_type const& other) const noexcept
596 : {
597 1 : return ctx_ == other.ctx_;
598 : }
599 :
600 : /** Compare two executors for inequality.
601 :
602 : @return `true` if the executors refer to different contexts.
603 : */
604 : bool operator!=(executor_type const& other) const noexcept
605 : {
606 : return ctx_ != other.ctx_;
607 : }
608 : };
609 :
610 : inline io_context::executor_type
611 937 : io_context::get_executor() const noexcept
612 : {
613 937 : return executor_type(const_cast<io_context&>(*this));
614 : }
615 :
616 : } // namespace boost::corosio
617 :
618 : #endif // BOOST_COROSIO_IO_CONTEXT_HPP
|