I/O Context
The io_context class is the heart of Corosio. It’s an event loop that
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()). The concurrency hint only
tunes performance and never changes the safety contract; single-threaded
(lockless) mode is opt-in exclusively via the single_threaded_lockless option
(see Single-Threaded Mode).
With Concurrency Hint
corosio::io_context ioc(1); // one-thread hint, still fully thread-safe
corosio::io_context ioc(4); // up to 4 threads
The concurrency hint affects:
-
Internal synchronization strategy
-
On Windows, the hint 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 never disables locking — a context built with concurrency_hint == 1
remains fully thread-safe, so other threads may post() into it. To drop
synchronization overhead in a genuinely single-threaded program, set
io_context_options::single_threaded_lockless = true instead.
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);
corosio::timer timer(ioc);
// Launch initial coroutine
capy::run_async(ioc.get_executor())(main_coroutine(sock, timer));
// 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 lockless mode: constructing with
io_context_options::single_threaded_lockless = true drops these guarantees (see
Single-Threaded Mode).
| 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, and 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 launched 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: stop submitting new work, 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 with zero-copy potential