Coroutines

FAQ

  1. When reading about coroutines, I often come across the term task. Within the context of C++, is a task a function or process?

    A task is a coroutine object representing an asynchronous operation that will eventually produce a result. A normal function runs immediately and returns a value. A task is a coroutine that will produce a value later when awaited. In simple terms it is a lazy async function call that hasn’t run yet (a paused function call). The name was chosen as it is the same concept as an async task in Rust, or an asyncio Task in Python.

  2. How does a coroutine task compare with std::future<T>?

    The concepts are similar in that they both represent a value that will exist later. However std:future is thread-based, and the result retrieved using future.get() - which will block other code until the value is retrieved. A task is event-loop/coroutine based, does not require its own thread, is retrieved using co-await task - and critically this is a non-blocking call - other code will keep running.

  3. When should I consider using coroutine tasks and when standard future constructs?

    If the coding scenario you are building is heavily into CPU parallelism, or a simple background job, then std::future and a thread model is probably your best bet. If your scenario is high-scale networking, say involving millions of operations, then tasks and coroutines should provide a solid solution.

  4. What are the core constructs of Boost.Asio, I believe a super-popular networking library?

    Yes Boost.Asio is a very popular networking library. It’s core constructs are an event loop (an io_context), async operations, completion handlers, executors, strands, I/O objects, and buffers. It is certainly not based on futures and threads, though they are optional usage styles.

    Note

    A strand guarantees that handlers do not run concurrently.

  5. Is it true that coroutines can never block, or get caught up in race conditions?

    No. A coroutine is a state machine that can suspend and resume. Nothing about it automatically guarantees safety. Care has to be taken with coroutines not to be, for example, providing wrappers around blocking calls or wrappers around CPU-heavy work hidden inside the coroutine. And as for race conditions, coroutines do not imply mutual exclusion, atomicity, serialization, nor thread-safety.

    Coroutines are scheduling points - they guarantee that execution can be suspended and resumed, and that no thread is blocked whilst awaiting async I/O. Using coroutines still requires good practices around shared objects. Coroutines make asynchronous code look sequential, but they do not remove concurrency — so all normal rules about blocking and race conditions still apply.

  6. Has anyone come up with a good design methodology for coroutines, such as graphs of state machines, or flow diagrams of some sort?

    This has become an active design space, because once you move from “functions” to “suspended state machines”, you naturally start needing different mental models. There isn’t one single universally accepted methodology, but there are strong, widely used approaches that map very well to coroutines. The most popular in practice are Sequence Diagrams, for example:

    Coroutine           io_context           Timer
        |                   |                 |
        |--- async_wait --->|                 |
        |                   |--- register --->|
        |     (suspend)     |                 |
        |                   |                 |
        |                   |   time passes   |
        |                   |                 |
        |<-- resume --------|<-- ready -------|
        |                   |                 |

    A sequence diagram answers the questions - who is doing what, who resumes me, what thread am I on?

    Another approach is a Structured Concurrency Graph, where you model coroutines as a task tree, for example:

          Parent task
            /    \
           /      \
       child A   child B
          |          |
       await      await
          \        /
           \      /
          completion

    This approach helps you reason on cancellation propagation, lifetime, error propagation, and task ownership.

    Other approaches that have proved helpful include Control-Flow Graphs where nodes are execution blocks and edges are control flow or suspend/resume transitions. State Machine diagrams may also help, in that almost every coroutine boils down to a state machine with suspension points as states. Something else to consider is Actor-style models where coroutines are treated as message-driven state machines (actors). This actor-based approach has found success in game engines and high-concurrency servers.

    In the real world of coroutine programming - most bugs come from not modelling coroutines explicitly at all!

  7. In the world of coroutines, what is meant by a "promise", and what would an example function look like?

    There is confusion over the use of the word "promise", as a coroutine "promise object" is certainly not the same thing as a std:promise from <future>. The promise object is a compiler-generated control object that manages the coroutine’s state, results, suspension, and lifetime. Here is a minimalistic but runnable example - noting that promise_type is a sub-struct of Task:

    #include <coroutine>
    #include <iostream>
    
    // --------------------------------------------------
    // Coroutine return object
    // --------------------------------------------------
    struct Task
    {
        // =====================================================
        // A minimal promise type contains the following 5 calls
        // =====================================================
        struct promise_type
        {
            // Called when the coroutine object is created, and returns the parent Task
            Task get_return_object()
            {
                return {};
            }
    
            // Suspend immediately at start?
            std::suspend_never initial_suspend()
            {
                return {};
            }
    
            // Suspend at end?
            std::suspend_never final_suspend() noexcept
            {
                return {};
            }
    
            // Handles co_return;
            void return_void()
            {
                std::cout << "co_return happened\n";
            }
    
            // Handles uncaught exceptions
            void unhandled_exception()
            {
                std::terminate();
            }
        };
    };
    
    // --------------------------------------------------
    // Coroutine function
    // --------------------------------------------------
    Task example()
    {
        std::cout << "Inside coroutine\n";
    
        co_return; // Invokes promise.return_void()
    }
    
    int main()
    {
        example();
    }

    Note: When compiling this sample, a modern compiler is recommended (Visual Studio 2022, GCC 12, Clang 15, or later), and make sure to set the C++ compiler standard to C++20, or later. In Microsoft Visual Studio, for example, locate the project properties:

    Set Language Standard

    Don’t confuse the promise type with std::promise - which ia a thread-to-thread value delivery mechanism.

  8. Am I right that the co_await function handles suspension and resumption?

    Yes, co_await works by calling three methods (await_ready, await_suspend, await_resume) that together control whether and how a coroutine pauses and resumes, for example:

    #include <coroutine>
    #include <iostream>
    
    // --------------------------------------------------
    // A minimal awaiter
    // --------------------------------------------------
    struct SimpleAwaiter
    {
        // Should we suspend?
        bool await_ready() const noexcept
        {
            return false;
        }
    
        // Called when suspending
        void await_suspend(std::coroutine_handle<> h) const
        {
            std::cout << "Coroutine suspended\n";
    
            // Immediately resume for demo purposes
            h.resume();
        }
    
        // Value returned from co_await
        void await_resume() const noexcept
        {
            std::cout << "Coroutine resumed\n";
        }
    };
    
    // --------------------------------------------------
    // Minimal coroutine return type
    // --------------------------------------------------
    struct Task
    {
        struct promise_type
        {
            Task get_return_object()
            {
                return {};
            }
    
            std::suspend_never initial_suspend()
            {
                return {};
            }
    
            std::suspend_never final_suspend() noexcept
            {
                return {};
            }
    
            void return_void()
            {
            }
    
            void unhandled_exception()
            {
                std::terminate();
            }
        };
    };
    
    // --------------------------------------------------
    // Coroutine function
    // --------------------------------------------------
    Task example()
    {
        std::cout << "Before co_await\n";
    
        co_await SimpleAwaiter{};
    
        std::cout << "After co_await\n";
    }
    
    // --------------------------------------------------
    // main()
    // --------------------------------------------------
    int main()
    {
        example();
    }
  9. How do I code a coroutine to produce a single value in the future, then close down?

    A co_return communicates with the coroutine’s promise object to produce a single value. For example, the following code returns an integer:

    #include <coroutine>
    #include <iostream>
    
    // --------------------------------------------------
    // Coroutine return object
    // --------------------------------------------------
    struct Task
    {
        struct promise_type
        {
            int value;
    
            // Create return object
            Task get_return_object()
            {
                return Task{
                    std::coroutine_handle<promise_type>::from_promise(*this)
                };
            }
    
            // Start immediately
            std::suspend_never initial_suspend()
            {
                return {};
            }
    
            // Suspend at end so result can be read
            std::suspend_always final_suspend() noexcept
            {
                return {};
            }
    
            // Called by: co_return int;
            void return_value(int v)
            {
                value = v;
            }
    
            void unhandled_exception()
            {
                std::terminate();
            }
        };
    
        // Store coroutine handle
        std::coroutine_handle<promise_type> handle;
    
        // Constructor
        Task(std::coroutine_handle<promise_type> h)
            : handle(h)
        {
        }
    
        // Destructor
        ~Task()
        {
            handle.destroy();
        }
    
        // Access returned value
        int result() const
        {
            return handle.promise().value;
        }
    };
    
    // --------------------------------------------------
    // Coroutine function
    // --------------------------------------------------
    Task compute()
    {
        std::cout << "Inside coroutine\n";
    
        co_return 101;
    }
    
    // --------------------------------------------------
    // main()
    // --------------------------------------------------
    int main()
    {
        Task task = compute();
    
        std::cout << "Returned value: "
                  << task.result()
                  << "\n";
    }
  10. How do I code a coroutine to produce a series of values, suspending after each value is released?

    The key construct to continuous delivery is co_yield; this pauses the coroutine and returns a value to the caller, but keeps the coroutine alive for later resumption. For example:

    #include <coroutine>
    #include <iostream>
    
    // --------------------------------------------------
    // Generator type
    // --------------------------------------------------
    struct Generator
    {
        struct promise_type
        {
            int current_value;
    
            Generator get_return_object()
            {
                return Generator{
                    std::coroutine_handle<promise_type>::from_promise(*this)
                };
            }
    
            std::suspend_always initial_suspend()
            {
                return {};
            }
    
            std::suspend_always final_suspend() noexcept
            {
                return {};
            }
    
            // Handles co_yield
            std::suspend_always yield_value(int value)
            {
                current_value = value;
                return {};
            }
    
            void return_void() {}
            void unhandled_exception()
            {
                std::terminate();
            }
        };
    
        std::coroutine_handle<promise_type> handle;
    
        explicit Generator(std::coroutine_handle<promise_type> h)
            : handle(h)
        {}
    
        ~Generator()
        {
            if (handle)
                handle.destroy();
        }
    
        // Move to next value
        bool next()
        {
            if (!handle || handle.done())
                return false;
    
            handle.resume();
            return !handle.done();
        }
    
        // Get current value
        int value() const
        {
            return handle.promise().current_value;
        }
    };
    
    // --------------------------------------------------
    // Coroutine producing values
    // --------------------------------------------------
    Generator counter(int start, int end)
    {
        for (int i = start; i <= end; ++i)
        {
            co_yield i;   // <-- THIS is the key point
        }
    }
    
    // --------------------------------------------------
    // main()
    // --------------------------------------------------
    int main()
    {
        auto gen = counter(1, 5);
    
        while (gen.next())
        {
            std::cout << gen.value() << "\n";
        }
    }

    If you run this code, you should get:

    1
    2
    3
    4
    5

    It might help to think of co_yield as a lazy generator, only returning values when asked. They are useful when streaming data. parsing token streams, working with async data pipelines, and efficient generators of data in game engines.

  11. Is an awaitable object more efficient than a std:future object?

    Depends on the use-case, but a coroutine awaitable object is non-blocking, does not require its own thread, there is no polling and no explicit shared state.

    An awaitable<T> is a Boost.Asio coroutine-based asynchronous return type that allows functions to produce values later, delivered seamlessly via co_await. The following example uses a timer to simulate async work:

    #include <boost/asio.hpp>
    #include <iostream>
    
    using namespace boost::asio;
    using namespace std::chrono_literals;
    
    // --------------------------------------------------
    // Coroutine returning a value (awaitable<int>)
    // --------------------------------------------------
    awaitable<int> delayed_value(int v)
    {
        auto executor = co_await this_coro::executor;
    
        steady_timer timer(executor);
    
        timer.expires_after(1s);
        co_await timer.async_wait(use_awaitable);
    
        co_return v * 3;   // <-- returns into awaitable<int>
    }
    
    // --------------------------------------------------
    // Caller coroutine
    // --------------------------------------------------
    awaitable<void> consumer()
    {
        std::cout << "Requesting value...\n";
    
        int result = co_await delayed_value(111);
    
        std::cout << "Got result: " << result << "\n";
    }
    
    // --------------------------------------------------
    // main()
    // --------------------------------------------------
    int main()
    {
        io_context io;
    
        co_spawn(io, consumer(), detached);
    
        io.run();
    }

    If you run this code, you should get:

    Requesting value...
    Got result: 333
  12. What is considered best practices for instrumenting a coroutine based program - what should be logged?

    Instrumenting coroutine-based code is a little different from instrumenting traditional synchronous code because control flow becomes non-linear. For long-running or important coroutines, log "coroutine x started" and "coroutine x completed". And don’t forget errors "coroutine x failed: timeout". The most valuable information in coroutine systems is often before and after co_await. Cancellation is frequently overlooked, make sure to log concellation requests, cancellation observed, and probably a cleanup complete message too. This is enormously helpful when investigating shutdown issues.

    For performance-based instrumentation, obviously add accurate timing to your logs, such as "Socket read completed in 2.1 ms".

    Large coroutine systems become nearly impossible to diagnose without corelation IDs - IDs that connect sockets, databases, timers and worker pools. A corelation ID should be generated and logged along with coroutine events such as requests, queueing or waiting, completion, response sent. Coroutine systems often hide queueing, so it will be helpful to have logs of "queued for 18 ms" and " exectured for 2 ms" - obviously to help you identify bottlenecks. Tracing resource ownership has also proved to be of value, as coroutines often make resource lifetimes less obvious because ownership spans suspension points.

    Consider not logging every suspend/resume event, as this will very quickly become noise.

    For Boost.Asio applications specifically, a common best practice is to trace:

    1. Coroutine start/end

    2. Every significant co_await of I/O operations

    3. Executor transitions (post, dispatch, thread-pool hops)

    4. Exceptions and cancellations

    5. Timing information around each awaited operation

    6. A request/session correlation ID carried through the coroutine chain

      Those six categories typically provide most of the diagnostic value with relatively little logging overhead.