I/O Context
The io_context class is the event loop every Corosio program needs: it
processes asynchronous I/O operations, manages timers, and coordinates
coroutine execution.
|
Code snippets assume:
|
Overview
Every Corosio program needs at least one io_context:
corosio::io_context ioc;
// ... create I/O objects and launch coroutines ...
ioc.run(); // Process events until all work completes
The io_context:
-
Owns the platform-specific I/O backend (IOCP on Windows)
-
Maintains a queue of pending work items
-
Provides an executor for coroutine dispatch
-
Tracks outstanding work to know when to stop
Construction
Default Construction
corosio::io_context ioc;
Creates an io_context with a concurrency hint of
std::max(1u, std::thread::hardware_concurrency()) and the default, fully
thread-safe locking tier. Thread safety is governed by the locking option
(see Locking Tiers).
With Concurrency Hint
corosio::io_context ioc(1); // one thread expected to call run()
corosio::io_context ioc(4); // up to 4 threads expected to call run()
The concurrency hint is a performance tuning value indicating how many threads
are expected to call run(). It tunes:
-
The reactor’s inline-completion budget defaults: a multi-thread context favors posting completions for cross-thread work-stealing, while a single-thread context keeps the inline fast path.
-
On Windows, it is passed to
CreateIoCompletionPortasNumberOfConcurrentThreads, bounding how many completion threads may run concurrently. It is not a library-managed thread pool, and it is distinct fromthread_pool_size.
The hint does not affect thread safety, which is governed by the locking
tier. A lockless locking tier (unsafe_io or unsafe) overrides
the effective hint to 1 regardless of the value passed. To reduce synchronization overhead in a
single-threaded program, select a lockless locking tier (see
Locking Tiers).
Running the Event Loop
run()
Processes all pending work until stopped:
std::size_t n = ioc.run();
std::cout << "Processed " << n << " handlers\n";
This function:
-
Blocks until all work completes or
stop()is called -
Returns the number of handlers executed
-
Automatically stops when no outstanding work remains
-
Returns immediately with
0if there is no outstanding work when called
run_one()
Processes at most one work item:
std::size_t n = ioc.run_one(); // Returns 0 or 1
Useful for manual event loop control or interleaving with other work.
Stopping and Restarting
The Executor
The io_context::executor_type provides the interface for dispatching work:
auto ex = ioc.get_executor();
// Launch a coroutine
capy::run_async(ex)(my_coroutine());
// Access the context
corosio::io_context& ctx = ex.context();
// Check if running inside the event loop
if (ex.running_in_this_thread())
std::cout << "Inside run()\n";
Executor Operations
auto ex = ioc.get_executor();
// Dispatch a continuation: symmetric transfer if inside run(),
// otherwise post. Returns a handle to resume.
std::coroutine_handle<> next = ex.dispatch(cont);
// Post a continuation: always queue for later execution
ex.post(cont);
// Post a bare coroutine handle for later execution
ex.post(handle);
The dispatch operation ex.dispatch(cont) enables symmetric transfer when
already running inside run(): it returns cont.h directly so the caller can
resume it inline. Otherwise it posts the continuation for later execution and
returns std::noop_coroutine(). This is how child coroutines resume parents
efficiently.
Typical Usage Pattern
int
main()
{
corosio::io_context ioc;
// Create I/O objects
corosio::tcp_socket sock(ioc);
// Launch initial coroutine
capy::run_async(ioc.get_executor())(main_coroutine(sock));
// Run until all work completes
ioc.run();
}
Thread Safety
The io_context is thread-safe by default, regardless of the concurrency hint:
multiple threads may call run() concurrently, and any thread may post() work
into it.
corosio::io_context ioc(4);
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i)
threads.emplace_back([&ioc] { ioc.run(); });
for (auto& t : threads)
t.join();
Multiple threads can call run() concurrently. The io_context distributes
work across threads. The one exception is a lockless locking tier: constructing
with locking set to unsafe_io or unsafe drops these
guarantees (see
Locking Tiers).
| Individual I/O objects (sockets, timers) are not thread-safe. Don’t access the same socket from multiple threads without synchronization. |
Lifetime and Teardown
The io_context must outlive every operation posted or dispatched through its
executor. No thread may still be inside a run() call when the context is
destroyed. Posting to the context — from a thread it does not track —
concurrently with, or after, its destruction is undefined behavior.
Work started with capy::run / capy::run_async is work-tracked, so a normal
run() completion already waits for it to finish. The safe teardown pattern is
therefore to stop submitting new work and let every run() return. Join the
threads that ran the loop, and only then destroy the context.
corosio::io_context ioc(4);
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i)
threads.emplace_back([&ioc] { ioc.run(); });
// Join every run() thread before ioc leaves scope.
for (auto& t : threads)
t.join();
stop() abandons pending work rather than draining it. Destroying the
context after stop() while other threads may still post to it is undefined
behavior — join those threads first.
|
Inheritance from execution_context
io_context inherits from capy::execution_context, providing service
management:
// Create or get a service
my_service& svc = ioc.use_service<my_service>();
// Check if service exists
if (ioc.has_service<my_service>())
{
// ...
}
Services are destroyed when the io_context is destroyed.
Platform Details
Windows (IOCP)
On Windows, the io_context uses I/O Completion Ports:
-
Scalable to thousands of concurrent connections
-
Efficient thread pool utilization
-
Native async I/O
Linux (epoll)
On Linux, the io_context uses epoll:
-
Scalable to large numbers of file descriptors
-
Edge-triggered notifications
-
Efficient for long-lived connections
macOS / FreeBSD (kqueue)
On macOS and FreeBSD, the io_context uses kqueue:
-
Efficient event notification
-
File descriptor monitoring
Next Steps
-
Sockets — I/O with TCP sockets
-
Acceptors — Accept incoming connections
-
Delays and Timeouts — Async delays and timeouts