Phase 4 · Advanced C++ & Systems ProgrammingModule 24~60 min read

Coroutines & Asynchronous Design

Understand C++ coroutine mechanics and build lifetime-aware generators and task abstractions for asynchronous workflows.

What you'll learn

A coroutine is a function that can suspend and resume while preserving its state. The language supplies transformation hooks, not a complete async runtime. You will inspect promise types, awaiters, handles, generators, tasks, cancellation, and the lifetime rules that make production coroutine code safe.

By the end, you'll be able to:

  • Explain coroutine state, suspension, and the three coroutine keywords
  • Describe promise, awaiter, and coroutine_handle responsibilities
  • Build and consume a minimal pull-style generator
  • Choose coroutines only with an explicit scheduler, lifetime, cancellation, and error model

Suspension and coroutine state

A function containing co_await, co_yield, or co_returncan be transformed into a coroutine. Its parameters, locals that survive suspension, promise object, and bookkeeping live in a coroutine frame. Calling it usually returns a wrapper that controls or observes that frame.

KeywordPurpose
co_await expressionPossibly suspend until an awaitable is ready
co_yield valuePublish a value and suspend, typically in a generator
co_return valueComplete the coroutine through its promise
coroutine_handleNon-owning handle used to resume, query, or destroy a frame

Key idea

Suspending a coroutine does not block a thread, but it also does not create concurrency by itself. Some scheduler, event source, or caller must arrange resumption.

Promise and awaiter protocol

The return type identifies a nested promise_type. The compiler asks that promise for the returned wrapper, initial and final suspension policy, yielded or returned values, and exception handling. An awaiter supplies await_ready, await_suspend, and await_resume.

awaiter_protocol.txt
co_await expression
  1. Convert expression to an awaiter.
  2. await_ready(): continue immediately if true.
  3. await_suspend(handle): publish/schedule the continuation.
  4. Suspend when the protocol says to.
  5. A scheduler or event resumes the coroutine.
  6. await_resume(): produce the co_await result or throw.

Watch out

The continuation handle passed to await_suspend is lifetime-sensitive. Resuming or destroying a frame twice, or resuming after destruction, is undefined behavior.

A minimal generator

A pull generator suspends at each co_yield. The wrapper owns the frame, resumes it to request another value, and destroys it at the end. Production generators need polished iterators and range integration; this focused version exposes the essential mechanics.

generator.cpp
#include <coroutine>
#include <exception>
#include <iostream>
#include <utility>

template <typename T>
class Generator {
public:
    struct promise_type {
        T current{};
        std::exception_ptr error;

        Generator get_return_object() {
            return Generator{handle_type::from_promise(*this)};
        }
        std::suspend_always initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        std::suspend_always yield_value(T value) noexcept {
            current = std::move(value);
            return {};
        }
        void return_void() noexcept {}
        void unhandled_exception() noexcept { error = std::current_exception(); }
    };

    using handle_type = std::coroutine_handle<promise_type>;
    explicit Generator(handle_type handle) : handle_{handle} {}
    Generator(const Generator&) = delete;
    Generator(Generator&& other) noexcept
        : handle_{std::exchange(other.handle_, {})} {}
    ~Generator() { if (handle_) handle_.destroy(); }

    bool next() {
        handle_.resume();
        if (handle_.done() && handle_.promise().error)
            std::rethrow_exception(handle_.promise().error);
        return !handle_.done();
    }
    const T& value() const { return handle_.promise().current; }

private:
    handle_type handle_;
};

Generator<int> count_to(int end) {
    for (int value{1}; value <= end; ++value) co_yield value;
}

int main() {
    auto values{count_to(3)};
    while (values.next()) std::cout << values.value() << ' ';
}

Tasks and schedulers

A task-like coroutine represents one eventual completion rather than a sequence. Its awaiter typically records the awaiting continuation and resumes it when the task finishes. An event loop, I/O completion system, or thread pool decides where and when execution resumes; the C++ standard library does not yet provide one universal production task type.

  • Define whether a task starts eagerly or only when awaited
  • State which executor or thread resumes each continuation
  • Propagate values and exceptions through the promise
  • Make frame ownership and destruction unambiguous
  • Avoid silently blocking inside an interface advertised as asynchronous

Note

Use a mature coroutine library or framework for networking and production async I/O. Its task, scheduler, cancellation, and lifetime contracts are more important than the surface co_await syntax.

Lifetime, cancellation, and errors

Local variables stored in the frame survive suspension, but referenced external objects do not gain lifetime. Cancellation must reach pending awaiters and prevent abandoned work from later resuming a destroyed frame. Exceptions are commonly captured byunhandled_exception and rethrown when a consumer awaits or advances the result.

RiskDesign response
Borrowed object dies while suspendedOwn needed state or use a verified shared lifetime
Operation is cancelledUnregister callbacks and complete with a documented cancellation result
Awaited operation failsStore exception or typed error and surface it at await_resume
Task wrapper is destroyedDefine whether it cancels, detaches, waits, or destroys safely
Continuation resumes on another threadDocument synchronization and executor affinity

Coroutines versus other tools

Coroutines excel at sequential-looking workflows with many suspension points and at lazy generators. Threads suit blocking independent work; futures suit one-shot results; callbacks integrate with event APIs but can fragment control flow; ranges suit synchronous lazy data transformations. Choose from the actual execution and ownership model.

ToolStrong fit
CoroutineSuspendable workflow or generator with an established runtime
jthreadIndependent blocking or CPU work with scoped lifetime
futureOne eventual result with simple waiting
CallbackExisting event-source integration and small continuations
Range viewSynchronous lazy transformation without suspension

Recap & quick check

Key takeaways

  • Coroutine state lives in a frame that survives suspension until an owner destroys it.
  • Promise types control results and suspension; awaiters control readiness, suspension, and resumption.
  • A generator owns its frame and resumes it to pull each yielded value.
  • C++ coroutine syntax does not provide a scheduler, event loop, or universal task abstraction.
  • Safe async design must define ownership, cancellation, error propagation, and resumption context.

Quick check

1. Does a coroutine automatically create a new thread?

2. Where do locals that survive suspension normally live?

3. Which awaiter method produces the co_await result?

4. What must production coroutine design define beyond syntax?

Next: Module 25 — Binary Data, Interoperability & Low-Level C++, where C++ meets bytes, native APIs, and platform boundaries.