**Understanding C++ Coroutines: A Modern Approach to Asynchronous Programming**

C++20 introduced coroutines as a language‑level feature that allows developers to write asynchronous code in a sequential style. A coroutine is a function that can suspend execution (co_await, co_yield, or co_return) and resume later, making it ideal for lazy evaluation, pipelines, and non‑blocking I/O. Below we explore the core concepts, a simple implementation, and best practices.


1. Coroutine Basics

Term Meaning
co_await Suspends until the awaited expression completes.
co_yield Produces a value to the caller and suspends.
co_return Returns a final value and ends the coroutine.
promise_type Provides the machinery that the compiler uses to manage coroutine state.
awaitable Any type that can be awaited; it must provide await_ready(), await_suspend(), and await_resume().

When you write a coroutine, the compiler generates a state machine that manages these suspensions. The return type of the coroutine is typically a specialization of std::future, std::generator, or a user‑defined type that hides the promise.


2. A Simple generator Example

Below is a minimal implementation of a generator that yields Fibonacci numbers. This demonstrates how to create an awaitable type and how to use co_yield.

#include <coroutine>
#include <iostream>
#include <optional>

template <typename T>
class generator {
public:
    struct promise_type;
    using handle_type = std::coroutine_handle <promise_type>;

    generator(handle_type h) : handle(h) {}
    ~generator() { if (handle) handle.destroy(); }

    // Iterator interface
    struct iterator {
        handle_type h;
        iterator(handle_type h_) : h(h_) { advance(); }
        iterator& operator++() { advance(); return *this; }
        const T& operator*() const { return *h.promise().current_value; }
        bool operator==(std::default_sentinel_t) const { return !h || h.done(); }

    private:
        void advance() { if (!h.done()) h.resume(); }
    };

    iterator begin() { return iterator(handle); }
    std::default_sentinel_t end() { return {}; }

private:
    handle_type handle;
};

template <typename T>
struct generator <T>::promise_type {
    std::optional <T> current_value;
    generator get_return_object() { return generator{handle_type::from_promise(*this)}; }
    std::suspend_always initial_suspend() { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    std::suspend_always yield_value(T value) {
        current_value = std::move(value);
        return {};
    }
    void return_void() {}
    void unhandled_exception() { std::terminate(); }
};

generator <uint64_t> fibonacci(unsigned n) {
    uint64_t a = 0, b = 1;
    for (unsigned i = 0; i < n; ++i) {
        co_yield a;
        auto tmp = a;
        a = b;
        b = tmp + b;
    }
}

Usage:

int main() {
    for (auto v : fibonacci(10)) {
        std::cout << v << ' ';
    }
    // Output: 0 1 1 2 3 5 8 13 21 34
}

3. co_await and awaitable

co_await works with types that satisfy the Awaitable concept. The compiler transforms the co_await expression into calls to:

await_ready()   // true → skip suspension
await_suspend() // receives the coroutine handle; returns true if suspension needed
await_resume()   // value returned after resumption

A trivial awaitable that completes immediately:

struct immediate_awaitable {
    bool await_ready() noexcept { return true; }
    void await_suspend(std::coroutine_handle<>) noexcept {}
    void await_resume() noexcept {}
};

async void example() {
    co_await immediate_awaitable{}; // no suspension
}

For I/O, you typically wrap platform APIs (e.g., Boost.Asio, libuv) into awaitables that resume when data arrives.


4. Common Pitfalls

Issue Fix
Exception safety Ensure promise_type::unhandled_exception handles or rethrows.
Lifetime of awaitables Avoid returning references to local objects from await_resume.
State machine size Keep coroutine functions small; large state machines hurt stack size.
Blocking inside coroutines Never call blocking APIs directly; wrap them in awaitables that resume asynchronously.

5. Where to Use Coroutines

  • Lazy sequences: generator types that produce values on demand.
  • Async I/O: Wrapping sockets, files, or database queries.
  • Reactive pipelines: Compose streams of data with co_yield.
  • Coroutines as continuations: Offload work to a thread pool with custom awaitables.

6. TL;DR

C++ coroutines provide a powerful abstraction for asynchronous and lazy programming. By understanding the promise type, awaitable interface, and coroutine control flow, you can write cleaner, non‑blocking code that integrates seamlessly with existing C++ ecosystems. Start small—create a generator, then evolve to complex I/O workflows—and you’ll unlock the full potential of modern C++.

发表评论