Ch23: Coroutines
Why This Chapter Exists
Every server in the previous chapters had the same hidden limitation: a thread can only do one thing at a time. The synchronous echo server accepts one client, writes to it, closes it, and only then accepts the next. A slow client blocks the server. The usual answer is a thread per connection, which works until the connections outnumber the threads the machine can afford.
Coroutines are the other answer. A coroutine is a function that can suspend in the middle and resume later, without owning a thread while it waits. The code still looks like a sequence of statements: accept, read, write, loop. The scheduler, not the programmer, decides which suspended coroutine runs next.
This chapter teaches the language mechanism first, then fast_io's async model, then a real TCP server. The last page turns the echo server into an HTTP server, where formatted I/O and raw byte transfer meet.
The Four Pages
The Short Version
A C++20 coroutine is a function containing co_await, co_yield or
co_return. The compiler turns it into a frame plus a promise. The frame holds
the parameters, the locals that cross a suspend point, and the resume point. The promise
decides what the coroutine returns, whether it starts lazily, what happens at the end, and
how errors are stored.
co_await uses the awaiter protocol: await_ready,
await_suspend, await_resume. Async I/O is that protocol plus a
scheduler. The coroutine submits an operation and suspends; the event loop waits for the
kernel, then resumes the stored handle. The code reads like a blocking sequence, but the
thread is free while the operation is in flight.
fast_io's async layer is four objects: an owning io_async_scheduler, a
non-owning io_async_observer, a lazy io_async_task<T>, and
the io_async_wait pump. The scheduler is io_uring on Linux, IOCP on Windows, or
a thread pool on other POSIX systems. The source code does not change.
Use async_scan for structured input, async_print for structured
output, and async_transmit_some_bytes / async_transmit_all_bytes
for byte movement. Drop to async_pread_some_bytes /
async_pwrite_some_bytes only when you are implementing the layer beneath those
operations. A server that reads lines should not be calling pread.
A coroutine is not a thread. Suspension is cooperative; blocking inside a coroutine blocks
every coroutine on the same event loop. The frame has a lifetime: keep an owned task alive
until it is done(), or detach() it and let it destroy itself.
Errors travel through unhandled_exception or
unhandled_herbception and are rethrown at the co_await site or by
rethrow_if_error().
The server pattern is two coroutines: an accept loop that owns the listener, and one detached
session per connection. async_accept returns an owning socket; passing it by
value transfers ownership into the session frame. The supervisor restarts the accept loop
when the listener dies. That is the whole architecture, and it scales because most sessions
are suspended, not because the program created more threads.
Key Takeaways
- A coroutine is a state machine, not a thread.
co_awaitsuspends the current operation; the scheduler resumes it.- fast_io’s async model uses
io_async_taskand theasync_*operations. - Use
async_transmitfor bytes andasync_scan/async_printfor values.