Backend Development 26 min read

Understanding Asio Coroutine Implementation in C++20

The article explains how Asio implements C++20 coroutine support by converting callback‑based async operations into awaitables with use_awaitable, launching them via co_spawn and co_spawn_entry_point, handling result propagation and thread dispatch, and providing parallel composition operators (&&, ||) for concurrent awaiting.

Tencent Cloud Developer
Tencent Cloud Developer
Tencent Cloud Developer
Understanding Asio Coroutine Implementation in C++20

After the C++20 standard was released, Asio added support for C++20 coroutines. Although the implementation is still young and has some rough edges, many of its design points are worth studying. This article focuses on the coroutine implementation used in the version demonstrated in the author's "Why C++20 is the Awesomest Language for Network Programming".

We start with a simple example that creates a timer, awaits its expiration using co_await , and returns a value:

asio::awaitable
coro_test(asio::io_context& ctx) {
  asio::steady_timer timer(ctx);
  timer.expires_after(1s);
  co_await timer.async_wait(asio::use_awaitable);
  co_return 43;
}

TEST(THREAD_CONTEXT_TEST, CROUTINE_TEST) {
  asio::io_context ctx{};
  auto wg = asio::make_work_guard(ctx);
  std::thread tmp_thread([&ctx] { ctx.run(); });
  asio::co_spawn(ctx, coro_test(ctx), [] (std::exception_ptr ex, int res) {
    std::cout << "value test:" << res << std::endl;
  });
  std::this_thread::sleep_for(5s);
}

The code demonstrates several key concepts:

We define a coroutine coro_test that returns an int .

A steady_timer is created and awaited with asio::use_awaitable .

The coroutine is launched with asio::co_spawn , and a callback receives the result.

Key questions arise from this example:

Why can a callback‑based steady_timer::async_wait() be awaited with co_await ?

What does co_spawn() do to make the coroutine scheduleable by Asio?

How is the value returned by co_return passed to the final callback?

To answer these, we examine the two main parts of Asio’s coroutine scheduling: asio::co_spawn() and co_spawn_entry_point() .

1. Implementation of asio::co_spawn()

co_spawn has several overloads; the version handling a return value is roughly:

template<typename Executor, typename T, typename AwaitableExecutor, typename CompletionToken>
inline auto co_spawn(const Executor& ex, awaitable<T, AwaitableExecutor> a, CompletionToken&& token) {
  return async_initiate<CompletionToken, void(std::exception_ptr, T)>(
      detail::initiate_co_spawn<AwaitableExecutor>(AwaitableExecutor(ex)),
      token, detail::awaitable_as_function<T, AwaitableExecutor>(std::move(a)));
}

This forwards the coroutine to detail::initiate_co_spawn , which ultimately creates an awaitable_handler and launches an awaitable_thread . The handler is responsible for attaching the coroutine to the Asio execution context, storing the result, and invoking the user‑provided callback.

2. co_spawn_entry_point() – wrapping the user coroutine

The entry point wraps the original coroutine ( f ) and the final callback ( handler ) into a new coroutine that manages error handling, result propagation, and cancellation. Its core logic looks like:

template<typename T, typename Executor, typename F, typename Handler>
auto co_spawn_entry_point(awaitable<T, Executor>*, Executor ex, F f, Handler handler) -> awaitable<awaitable_thread_entry_point, Executor> {
  auto spawn_work = make_co_spawn_work_guard(ex);
  auto handler_work = make_co_spawn_work_guard(asio::get_associated_executor(handler, ex));
  (void) co_await (dispatch)(use_awaitable_t<Executor>{__FILE__, __LINE__, "co_spawn_entry_point"});
  (co_await awaitable_thread_has_context_switched{}) = false;
  std::exception_ptr e = nullptr;
  bool done = false;
  try {
    T t = co_await f();
    done = true;
    if (co_await awaitable_thread_has_context_switched{}) {
      (dispatch)(handler_work.get_executor(), [handler = std::move(handler), t = std::move(t)]() mutable {
        std::move(handler)(std::exception_ptr(), std::move(t));
      });
    } else {
      (post)(handler_work.get_executor(), [handler = std::move(handler), t = std::move(t)]() mutable {
        std::move(handler)(std::exception_ptr(), std::move(t));
      });
    }
    co_return;
  } catch (...) {
    if (done) throw;
    e = std::current_exception();
  }
  if (co_await awaitable_thread_has_context_switched{}) {
    (dispatch)(handler_work.get_executor(), [handler = std::move(handler), e]() mutable {
      std::move(handler)(e, T());
    });
  } else {
    (post)(handler_work.get_executor(), [handler = std::move(handler), e]() mutable {
      std::move(handler)(e, T());
    });
  }
}

The function performs three essential tasks:

It obtains the currently executing coroutine (the “parent”) so that the child coroutine can be linked to it.

It creates a handler object that will be called when the child coroutine finishes, passing either the result or an exception.

It uses awaitable_thread_has_context_switched to decide whether to resume via dispatch (same thread) or post (different thread).

When the child coroutine finishes, its final_suspend() triggers pop_frame() , which removes the child from the frame stack and resumes the parent.

3. Awaitable Transformations and use_awaitable

Asio provides a special token asio::use_awaitable that converts a traditional callback‑based async operation into an awaitable object. Internally this is achieved by specializing async_result for use_awaitable_t :

template<typename Executor, typename R, typename... Args>
class async_result
, R(Args...)> {
public:
  using handler_type = detail::awaitable_handler<Executor, typename std::decay
::type...>;
  using return_type = typename handler_type::awaitable_type;
  // ... do_init and initiate create the handler, start the operation, and then suspend forever
};

The handler’s operator() stores the result, clears cancellation, pops the frame, and calls pump() to continue execution.

4. Combining Awaitables with && and ||

Asio also overloads operator&& and operator|| for awaitables, enabling parallel composition:

a && b runs both awaitables and returns a std::tuple of their results; if either fails, the other is cancelled.

a || b runs both and returns a std::variant indicating which one succeeded first; the loser is cancelled.

Both operators are implemented using make_parallel_group with appropriate wait conditions ( wait_for_one_error or wait_for_one_success ).

5. Simple Example Using &&

asio::awaitable
timer_delay1(asio::io_context& ctx) {
  asio::steady_timer timer(ctx);
  timer.expires_after(1s);
  co_await timer.async_wait(asio::use_awaitable);
  co_return 1;
}

asio::awaitable
timer_delay2(asio::io_context& ctx) {
  asio::steady_timer timer(ctx);
  timer.expires_after(2s);
  co_await timer.async_wait(asio::use_awaitable);
  co_return 2;
}

asio::awaitable
watchdog2(asio::io_context& ctx) {
  using namespace asio::experimental::awaitable_operators;
  std::tuple
mm = co_await (timer_delay2(ctx) && timer_delay1(ctx));
  co_return 43;
}

TEST(THREAD_CONTEXT_TEST, CROUTINE_TEST) {
  asio::co_spawn(ctx, watchdog2(ctx), [] (std::exception_ptr ex, int res) {
    std::cout << "abcd:" << res << std::endl;
  });
}

This demonstrates how two timers can be awaited concurrently and their results combined.

6. Summary

Asio’s coroutine support bridges the classic callback model and modern C++20 coroutines. The implementation relies on wrapping user coroutines, managing a frame stack, and providing special awaitable tokens. While powerful, the code is heavily wrapped and can be hard to follow, suggesting that for complex projects a higher‑level abstraction or a dedicated scheduler may be preferable.

cAsyncNetwork ProgrammingcoroutineC++20asioawaitable
Tencent Cloud Developer
Written by

Tencent Cloud Developer

Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.