How to Use asio::spawn for Stackful Coroutines in C++
Use asio::spawn to launch stackful coroutines that suspend execution using basic_yield_context as a completion token, allowing asynchronous operations to appear synchronous while maintaining full compatibility with Asio's executor model.
The asio::spawn function serves as the primary entry point for stackful coroutines in the Asio networking library, leveraging Boost.Coroutine (or Boost.Context) to maintain execution context across asynchronous operations. Unlike C++20 stackless coroutines that use co_await, asio::spawn utilizes basic_yield_context as a completion token that automatically resumes the coroutine when operations complete. This implementation, found in include/asio/spawn.hpp, allows developers to write sequential code that runs asynchronously on any Asio executor.
Core Architectural Components
Understanding the internal machinery of asio::spawn requires familiarity with three fundamental types defined in the Asio headers.
spawned_thread_base
The spawned_thread_base class provides the low-level suspension and resumption machinery for stackful coroutines. Defined in include/asio/spawn.hpp at lines 35–50, this abstract base handles the coroutine’s cancellation state and ownership semantics. It serves as the foundation upon which the coroutine driver switches contexts when an asynchronous operation completes.
basic_yield_context
The basic_yield_context<Executor> template acts as the completion token representing the currently running coroutine. Constructed with a pointer to the underlying spawned_thread_base and the associated executor, this type is defined around lines 62–130 in include/asio/spawn.hpp. It exposes critical methods including get_executor() for accessing the bound executor, reset_cancellation_state() for renewing cancellation tracking, and cancelled() for querying termination requests.
initiate_spawn
The initiate_spawn<Executor> template bridges the coroutine creation with Asio’s async_initiate mechanism. Declared in spawn.hpp and implemented in include/asio/impl/spawn.hpp, this initiation object constructs a concrete spawned_thread_base derived type and binds the user-provided function to the asynchronous execution context.
Launching Your First Coroutine
To create a stackful coroutine, invoke asio::spawn with an execution context (or executor) and a callable accepting asio::yield_context. The coroutine suspends by passing the yield token to asynchronous operations, which resume execution upon completion.
#include <asio.hpp>
#include <iostream>
void echo_server(asio::yield_context yield)
{
try
{
asio::ip::tcp::acceptor acceptor(yield.get_executor(),
asio::ip::tcp::endpoint(asio::ip::tcp::v4(), 12345));
for (;;)
{
asio::ip::tcp::socket socket(yield.get_executor());
acceptor.async_accept(socket, yield);
char data[512];
for (;;)
{
std::size_t n = socket.async_read_some(
asio::buffer(data), yield);
if (n == 0) break;
asio::async_write(socket,
asio::buffer(data, n), yield);
}
}
}
catch (std::exception& e)
{
std::cerr << "Echo server error: " << e.what() << "\n";
}
}
int main()
{
asio::io_context io;
asio::spawn(io, echo_server);
io.run();
}
Key implementation details:
asio::spawn(io, echo_server)binds the coroutine toio’s default executor (lines 71–104 inspawn.hpp).- Each
async_accept,async_read_some, andasync_writecall receivesyieldas its completion token, causing suspension until the operation finishes.
Serializing Execution with Strands
When coroutines must not execute concurrently with other completion handlers, bind them to a strand to enforce serialization. The coroutine inherits the strand’s executor, ensuring all asynchronous operations within the function execute sequentially relative to other strand-bound work.
void serial_task(asio::yield_context yield)
{
// All operations here execute on the strand without concurrent interference
}
int main()
{
asio::io_context io;
asio::strand<asio::io_context::executor_type> strand(io.get_executor());
asio::spawn(strand, serial_task);
io.run();
}
Error Handling with yield[ec]
By default, basic_yield_context throws asio::system_error when asynchronous operations fail. To capture error codes instead, use the operator[] overload defined at lines 108–113 in include/asio/spawn.hpp.
void client(asio::yield_context yield)
{
asio::ip::tcp::socket socket(yield.get_executor());
asio::error_code ec;
socket.async_connect(
asio::ip::tcp::endpoint(
asio::ip::make_address("127.0.0.1"), 12345),
yield[ec]);
if (ec)
{
std::cerr << "Connect failed: " << ec.message() << "\n";
return;
}
}
Implementing Cooperative Cancellation
Stackful coroutines support cooperative cancellation through the cancellation_state object accessible via basic_yield_context. The coroutine can query its cancellation status using yield.cancelled() or reset the state with reset_cancellation_state().
void cancellable_task(asio::yield_context yield)
{
for (int i = 0; i < 10; ++i)
{
if (yield.cancelled() != asio::cancellation_type::none)
throw asio::system_error(asio::error::operation_aborted);
asio::steady_timer timer(yield.get_executor(),
std::chrono::seconds(1));
timer.async_wait(yield);
}
}
int main()
{
asio::io_context io;
asio::cancellation_signal sig;
asio::spawn(io,
[&](asio::yield_context y){ cancellable_task(y); },
asio::detached);
std::thread([&]{
std::this_thread::sleep_for(std::chrono::seconds(3));
sig.emit(asio::cancellation_type::terminal);
}).detach();
io.run();
}
The cancellation slot is accessed indirectly through yield.get_cancellation_slot(), allowing external signals to propagate termination requests into the coroutine.
Key Implementation Files
The following headers contain the definitive implementation of Asio’s stackful coroutine support:
include/asio/spawn.hpp– Public interface exposingasio::spawn,basic_yield_context, andspawned_thread_base.include/asio/impl/spawn.hpp– Internal implementation ofdetail::initiate_spawnand coroutine lifecycle management.include/asio/cancellation_signal.hpp– Definescancellation_signalfor external termination requests.include/asio/cancellation_state.hpp– Per-coroutine cancellation state storage accessed viabasic_yield_context.
Summary
asio::spawncreates stackful coroutines using Boost.Coroutine under the hood, distinct from C++20co_spawnfor stackless coroutines.basic_yield_contextserves as the completion token that suspends execution and resumes automatically when asynchronous operations complete.- Strand binding ensures thread-safe, serialized execution by passing a strand as the first argument to
spawn. - Error handling defaults to exceptions but supports error codes via
yield[ec]. - Cancellation is cooperative, requiring the coroutine to check
yield.cancelled()or attach to cancellation slots.
Frequently Asked Questions
What is the difference between asio::spawn and asio::co_spawn?
asio::spawn implements stackful coroutines using Boost.Coroutine, where the entire call stack is preserved across suspension points via basic_yield_context. asio::co_spawn implements C++20 stackless coroutines using co_await, which do not preserve the call stack and rely on compiler-generated state machines. Use spawn when you need traditional stack preservation and yield semantics, and co_spawn when targeting C++20 coroutine syntax.
How does basic_yield_context handle suspension?
When passed to an asynchronous operation such as async_read, the basic_yield_context object stores the current coroutine’s state in the underlying spawned_thread_base and yields control back to the Asio event loop. The operation’s completion handler then invokes the coroutine’s resumption mechanism, restoring the stack and continuing execution with the operation’s result. This process is managed by the initiate_spawn implementation in include/asio/impl/spawn.hpp.
Can I use asio::spawn with custom executors?
Yes. The spawn function templates accept any type meeting the Executor requirements, including user-defined executors. The executor is stored within the basic_yield_context and retrieved via yield.get_executor(), ensuring that all subsequent asynchronous operations execute on the specified execution context.
How do I check for cancellation in a coroutine?
Call yield.cancelled() to obtain the current cancellation_type enum value. If the result is not asio::cancellation_type::none, the coroutine should terminate gracefully or reset its state via reset_cancellation_state(). The cancellation slot itself is accessible through yield.get_cancellation_slot(), allowing the coroutine to register handlers for specific cancellation signals.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →