ASIO Coroutine Support Features: From Stackless Macros to C++20 Awaitables

ASIO provides three distinct coroutine models: stackless macros for pre‑C++20 compilers, native C++20 awaitable coroutines with co_spawn, and Boost.Fiber‑style spawn functions, all supporting cancellation and executor propagation.

The chriskohlhoff/asio library ships with a comprehensive coroutine toolkit that spans legacy compilers lacking C++20 support all the way to modern asynchronous code using co_await. Understanding ASIO's coroutine support features helps you choose the right abstraction for your networking code, whether you need lightweight state machines or fully structured concurrency.

Stackless Coroutines (Macro-Based)

ASIO implements stackless coroutines through a set of macros that transform a class into a state machine using a single int state and a switch statement. This model requires no compiler coroutine support and lives in [asio/coroutine.hpp](https://github.com/chriskohlhoff/asio/blob/master/include/asio/coroutine.hpp).

How Stackless Coroutines Work

The mechanism relies on three pseudo‑keywords:

  • reenter (this) – Defines the coroutine body and initializes the state machine.
  • yield – Saves the current state, initiates an asynchronous operation, and suspends execution until completion.
  • fork – Creates child coroutines (checkable via is_child() and is_parent()).

Classes inherit from asio::coroutine and implement operator() with an error code and bytes‑transferred parameter. The macro expansion generates a switch statement that jumps to the correct case label on re‑entry.

#include <asio.hpp>
#include <iostream>

class echo_session : public asio::coroutine
{
public:
    echo_session(asio::ip::tcp::socket socket)
        : socket_(std::move(socket)) {}

    void operator()(asio::error_code ec = {}, std::size_t n = 0)
    {
        reenter (this)
        {
            for (;;)
            {
                yield socket_.async_read_some(
                    asio::buffer(data_), *this);
                
                yield asio::async_write(
                    socket_, asio::buffer(data_, n), *this);
            }
        }
    }

private:
    asio::ip::tcp::socket socket_;
    char data_[1024];
};

C++20 Coroutine Support

Modern ASIO leverages the C++20 coroutine language features through the awaitable return type and associated helpers in [asio/awaitable.hpp](https://github.com/chriskohlhoff/asio/blob/master/include/asio/awaitable.hpp) and [asio/co_spawn.hpp](https://github.com/chriskohlhoff/asio/blob/master/include/asio/co_spawn.hpp).

The awaitable Return Type

awaitable<T, Executor> is a completion token that tells ASIO to use the co_await keyword. When you mark a function with awaitable<std::size_t>, the compiler generates a coroutine frame that suspends at co_await points and resumes via ASIO’s internal machinery.

Use asio::use_awaitable as the completion token to trigger this behavior:

asio::awaitable<std::size_t> echo(asio::ip::tcp::socket socket)
{
    std::size_t total = 0;
    char data[1024];

    try
    {
        for (;;)
        {
            std::size_t n = co_await socket.async_read_some(
                asio::buffer(data), asio::use_awaitable);

            co_await asio::async_write(
                socket, asio::buffer(data, n), asio::use_awaitable);

            total += n;
        }
    }
    catch (std::exception const&) { }

    co_return total;
}

Launching Coroutines with co_spawn

co_spawn bridges awaitable functions with ASIO’s executor model. It takes an executor, a callable returning awaitable<T>, and a completion token (or handler) to receive the final result or exception.

asio::co_spawn(
    ctx,
    echo(std::move(sock)),
    [](std::exception_ptr e, std::size_t n)
    {
        if (!e) std::cout << "Transferred " << n << " bytes\n";
    });

Boost.Fiber Integration with spawn

[asio/spawn.hpp](https://github.com/chriskohlhoff/asio/blob/master/include/asio/spawn.hpp) provides asio::spawn, a higher‑level API that works with both the legacy stackless model (yield_context) and C++20 coroutines. Internally it uses Boost.Fiber or Boost.Context to manage stackful coroutines.

You can wrap a C++20 awaitable inside spawn for fire‑and‑forget scenarios:

asio::spawn(
    ctx,
    [sock = std::move(sock)]() mutable -> asio::awaitable<void>
    {
        co_await async_echo(std::move(sock));
    },
    asio::detached);

The detached completion token discards the result, making this pattern ideal for server loops that accept connections indefinitely.

Cancellation and Executor Access

Per‑coroutine metadata lives in [asio/this_coro.hpp](https://github.com/chriskohlhoff/asio/blob/master/include/asio/this_coro.hpp). This header provides the this_coro namespace for runtime introspection.

Per-Coroutine Cancellation State

Call asio::this_coro::cancellation_state() to query the current cancellation status, or asio::this_coro::reset_cancellation_state() to clear it. Both macro‑based and C++20 coroutines respect these calls, allowing you to implement graceful shutdowns.

Executor Propagation

awaitable carries an associated executor (defaulting to any_io_executor). Inside a coroutine, retrieve the current executor with:

auto ex = co_await asio::this_coro::executor;

This ensures that nested operations run on the correct strand or thread pool without explicit executor passing.

Summary

  • Stackless macros (reenter, yield, fork) in asio/coroutine.hpp provide lightweight state machines for pre‑C++20 compilers.
  • C++20 coroutines use awaitable<T> from asio/awaitable.hpp and are launched via co_spawn from asio/co_spawn.hpp.
  • asio::spawn in asio/spawn.hpp offers a Fiber‑compatible wrapper that bridges both worlds.
  • Cancellation and executors are accessible via asio/this_coro.hpp using cancellation_state() and executor.
  • All three models integrate seamlessly with ASIO’s executor and I/O object model.

Frequently Asked Questions

What is the difference between stackless and C++20 coroutines in ASIO?

Stackless coroutines are implemented as macros that generate a switch statement around a hidden state variable; they require inheritance from asio::coroutine and use yield for suspension. C++20 coroutines are compiler‑generated state machines using co_await and co_return with the awaitable return type, providing cleaner syntax and type safety.

How do I handle cancellation in ASIO coroutines?

Include asio/this_coro.hpp and call asio::this_coro::cancellation_state() to check if the coroutine has been cancelled. You can also reset the state with reset_cancellation_state(). Both the macro‑based and C++20 APIs respect the cancellation slot associated with the coroutine’s executor.

Can I mix stackless macros and C++20 awaitables in the same project?

Yes. ASIO’s design keeps these two models orthogonal. You can use stackless coroutines for legacy components while writing new code with awaitable and co_spawn, provided you compile the C++20 parts with a conforming compiler. The asio::spawn function can even wrap awaitable coroutines for compatibility with older code expecting yield_context.

Which header should I include for basic C++20 coroutine support?

Include asio/awaitable.hpp for the return type and asio/co_spawn.hpp to launch coroutines. If you need cancellation or executor access, also include asio/this_coro.hpp. For the completion token, use asio::use_awaitable defined in asio.hpp or asio/use_awaitable.hpp.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →