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:

#include <boost/corosio/io_context.hpp>
#include <boost/capy/ex/run_async.hpp>

namespace corosio = boost::corosio;
namespace capy    = boost::capy;

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 CreateIoCompletionPort as NumberOfConcurrentThreads, bounding how many completion threads may run concurrently. It is not a library-managed thread pool, and it is distinct from thread_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 0 if 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.

run_for() / run_until()

Process work for a limited time:

using namespace std::chrono_literals;

auto n = ioc.run_for(100ms);      // Run for 100 milliseconds
auto m = ioc.run_until(deadline); // Run until time point

These return the number of handlers executed within the time limit.

poll() / poll_one()

Process ready work without blocking:

std::size_t n = ioc.poll();     // All ready handlers
std::size_t m = ioc.poll_one(); // At most one ready handler

These never block waiting for I/O. Useful for games or GUI applications that need to pump events between frames.

Stopping and Restarting

stop()

Signal the event loop to stop:

ioc.stop();

This causes run() to return as soon as possible. Pending work remains queued but won’t be processed.

stopped()

Check whether the context stopped:

if (ioc.stopped())
    std::cout << "Event loop stopped\n";

restart()

Reset the stopped state:

ioc.stop();
// ... do something ...
ioc.restart();
ioc.run(); // Can run again

You must call restart() before calling run() again after it returns.

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.

Work Tracking

ex.on_work_started();  // Increment work count
ex.on_work_finished(); // Decrement work count

The event loop runs while the work count is non-zero. I/O objects and coroutines track work automatically.

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

io_uring

Linux also provides an io_uring backend:

  • Kernel-level async I/O

  • Reduced system calls

  • Support for more operation types

macOS / FreeBSD (kqueue)

On macOS and FreeBSD, the io_context uses kqueue:

  • Efficient event notification

  • File descriptor monitoring

Next Steps