Boost.Asio
C++
io_context
spawn
concurrency

When must you pass io_context to boostasiospawn? C

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

boost::asio::spawn() launches a stackful coroutine that runs on an executor. You must pass an io_context (or its executor) when the coroutine is not associated with an existing Asio object that already has an executor. If you spawn from within a handler that is already bound to an executor (like a socket's executor), you can pass that executor or the socket's strand instead. The rule is simple: spawn needs to know which executor to run the coroutine on.

When to Pass io_context

cpp
1#include <boost/asio.hpp>
2#include <boost/asio/spawn.hpp>
3
4int main() {
5    boost::asio::io_context io;
6
7    // Must pass io_context — no existing executor context
8    boost::asio::spawn(io, [](boost::asio::yield_context yield) {
9        // Coroutine body runs on io's executor
10        boost::asio::steady_timer timer(yield.get_executor(), std::chrono::seconds(1));
11        timer.async_wait(yield);
12        std::cout << "Timer fired!\n";
13    });
14
15    io.run();
16}

When spawn is called from main() or any non-Asio context, there is no implicit executor. You must provide io_context explicitly.

When You Can Use an Existing Executor

cpp
1boost::asio::spawn(io, [&](boost::asio::yield_context yield) {
2    boost::asio::ip::tcp::socket socket(io);
3
4    // Inside a coroutine, spawn a new coroutine on the same executor
5    // Can use the yield_context's executor instead of io_context
6    boost::asio::spawn(yield, [](boost::asio::yield_context yield2) {
7        boost::asio::steady_timer t(yield2.get_executor(), std::chrono::seconds(2));
8        t.async_wait(yield2);
9        std::cout << "Nested coroutine done\n";
10    });
11});

When spawning from within a coroutine, you can pass the yield_context directly. The new coroutine inherits the same executor.

Using a Strand

cpp
1boost::asio::io_context io;
2auto strand = boost::asio::make_strand(io);
3
4// Spawn on a strand for serialized execution
5boost::asio::spawn(strand, [](boost::asio::yield_context yield) {
6    // All coroutine operations run through the strand
7    // No concurrent execution with other handlers on the same strand
8});
9
10io.run();

Passing a strand instead of io_context ensures the coroutine's continuation handlers are serialized with other handlers on that strand. This is critical for thread safety when multiple threads call io.run().

Multiple Threads — Why Strands Matter

cpp
1boost::asio::io_context io;
2auto strand = boost::asio::make_strand(io);
3
4// Shared state
5int counter = 0;
6
7// Without strand — DATA RACE
8boost::asio::spawn(io, [&counter](boost::asio::yield_context yield) {
9    counter++;  // Unsafe if multiple threads run io.run()
10});
11
12// With strand — SAFE
13boost::asio::spawn(strand, [&counter](boost::asio::yield_context yield) {
14    counter++;  // Serialized — no data race
15});
16
17// Run on 4 threads
18std::vector<std::thread> threads;
19for (int i = 0; i < 4; i++) {
20    threads.emplace_back([&io] { io.run(); });
21}
22for (auto& t : threads) t.join();

Spawn Overloads

cpp
1// 1. Pass io_context
2boost::asio::spawn(io, handler);
3
4// 2. Pass executor (from io_context or strand)
5boost::asio::spawn(io.get_executor(), handler);
6boost::asio::spawn(strand, handler);
7
8// 3. Pass yield_context (inherit executor from parent coroutine)
9boost::asio::spawn(yield, handler);
10
11// 4. Pass with completion token (Boost.Asio 1.80+)
12boost::asio::spawn(io, handler, boost::asio::detached);

The completion token (detached) tells Asio what to do when the coroutine completes. Without it, older versions default to throwing on error.

TCP Server Example

cpp
1void session(boost::asio::ip::tcp::socket socket, boost::asio::yield_context yield) {
2    char buf[1024];
3    boost::system::error_code ec;
4
5    for (;;) {
6        std::size_t n = socket.async_read_some(
7            boost::asio::buffer(buf), yield[ec]);
8
9        if (ec) break;
10
11        boost::asio::async_write(
12            socket, boost::asio::buffer(buf, n), yield[ec]);
13
14        if (ec) break;
15    }
16}
17
18void listener(boost::asio::io_context& io, unsigned short port,
19              boost::asio::yield_context yield) {
20    boost::asio::ip::tcp::acceptor acceptor(
21        io, {boost::asio::ip::tcp::v4(), port});
22
23    for (;;) {
24        boost::system::error_code ec;
25        auto socket = acceptor.async_accept(yield[ec]);
26        if (ec) break;
27
28        // Spawn a new coroutine for each connection
29        // Use socket's executor to ensure proper context
30        boost::asio::spawn(socket.get_executor(),
31            [s = std::move(socket)](boost::asio::yield_context yield) mutable {
32                session(std::move(s), yield);
33            });
34    }
35}
36
37int main() {
38    boost::asio::io_context io;
39
40    // Must pass io_context — starting from main
41    boost::asio::spawn(io,
42        [&io](boost::asio::yield_context yield) {
43            listener(io, 8080, yield);
44        });
45
46    io.run();
47}

Error Handling

cpp
1boost::asio::spawn(io, [](boost::asio::yield_context yield) {
2    boost::system::error_code ec;
3    boost::asio::steady_timer timer(yield.get_executor(), std::chrono::seconds(5));
4
5    // Option 1: Exception (default)
6    try {
7        timer.async_wait(yield);
8    } catch (boost::system::system_error& e) {
9        std::cerr << "Error: " << e.what() << "\n";
10    }
11
12    // Option 2: Error code (no exception)
13    timer.async_wait(yield[ec]);
14    if (ec) {
15        std::cerr << "Error: " << ec.message() << "\n";
16    }
17});

yield[ec] captures the error code instead of throwing. This is the preferred pattern for performance-sensitive code.

Common Pitfalls

  • Forgetting to call io.run(): Coroutines spawned on an io_context do not run until io.run() is called. The program exits immediately without it.
  • Spawning on a destroyed io_context: If the io_context goes out of scope while coroutines are running, behavior is undefined. Keep the io_context alive until all work is done.
  • No strand with multiple threads: Spawning directly on io_context with multiple threads calling io.run() causes data races in coroutine continuations. Use make_strand() when sharing state.
  • Mixing stackful and stackless coroutines: boost::asio::spawn creates stackful coroutines (Boost.Context). C++20 co_await creates stackless coroutines. They use different mechanisms and should not be confused.
  • Large stack allocation: Each stackful coroutine allocates a stack (default 64KB-1MB depending on platform). Spawning thousands of coroutines can consume significant memory. Use boost::asio::spawn with a custom stack allocator for high-concurrency scenarios.

Summary

  • Pass io_context to spawn when there is no existing executor (e.g., from main())
  • Pass a strand for thread-safe coroutine execution with multiple threads
  • Pass yield_context from a parent coroutine to inherit its executor
  • Use yield[ec] for error code-based error handling instead of exceptions
  • Always use strands when coroutines share mutable state across threads
  • Each stackful coroutine allocates its own stack — be mindful of memory with many coroutines

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions