C++20 协程 Coroutine
本文最后更新于 33 天前,其中的信息可能已经有所发展或是发生改变。

本文基于 cppreference.com – Coroutines (C++20) 的内容整理扩展而成。

1. 协程介绍

1.1 什么是协程

协程(Coroutine) 是一种可以在执行过程中暂停并恢复的函数。与普通函数不同,协程不是一次性运行到结束,而是可以在某个点“挂起”(suspend),将控制权交还给调用者;之后再从挂起点继续执行。

特性概述:

  • 栈无关:协程不依赖于调用栈来保存状态。当它挂起时,其局部变量和执行位置被保存在一个独立分配的“协程状态对象”中。这使得协程的状态可以长期存在,甚至跨线程传递。
  • 支持异步编程模型:通过 co_await 关键字,开发者可以用同步的方式编写异步代码,显著降低复杂度,避免回调嵌套带来的“回调地狱”。
  • 适用于生成器模式:利用 co_yield 可以轻松实现一个按需生成数据的序列,如无限数列或流式数据处理。
  • 支持语言级原语:C++20 将 co_awaitco_yieldco_return 作为关键字引入,编译器会根据这些关键字自动生成相应的协程控制逻辑。

示例

// 示例:一个简单的异步 TCP 回显服务器
task<> tcp_echo_server() {
    char data[1024];
    while (true) {
        std::size_t n = co_await socket.async_read_some(buffer(data));
        co_await async_write(socket, buffer(data, n));
    }
}

这段代码看起来像是同步阻塞风格,但实际上每个 co_await 都可能使协程挂起,直到 I/O 操作完成后再恢复执行。整个过程不会阻塞线程,实现了高效的非阻塞通信。

1.2 如何定义协程

一个函数要成为协程,必须在其函数体内使用以下三个关键字之一:

关键字 含义
co_await 暂停执行,等待某个操作完成,常用于异步任务
co_yield 返回一个值并暂停,用于生成器(generator)模式
co_return 结束协程并返回结果

只要出现其中任意一个,该函数就被视为协程。

后文将对这三个关键字进行阐述。

示例:

// 是协程 — 使用了 co_yield
// generator<T> 用于惰性生成序列
generator<int> iota(int start = 0) {
    while (true)
        co_yield start++;
}

// 是协程 — 使用了 co_await
// task<T> 用于异步操作
task<void> fetch_data() {
    auto result = co_await http_client.get("/api/data");
    process(result);
}

// 是协程 — 使用了 co_return
// lazy<T> 延迟计算结果
lazy<int> compute() {
    co_return 42;
}

// 不是协程 — 虽然返回类型是 task,但没有使用任何协程关键字
task<void> not_a_coroutine() {
    return;
}

注意:可以看到返回类型比较特殊,但并不与协程挂钩,仅靠返回类型无法判断是否为协程!必须有 co_await / co_yield / co_return 才能触发编译器生成协程逻辑。

1.3 协程的限制

由于协程涉及复杂的生命周期管理和状态转移,C++ 对其施加了一些限制:

禁止使用的特性及其原因:

特性 原因说明
可变参数(... 参数复制和移动过程在协程状态下难以统一处理,容易引发未定义行为
普通 return 语句 协程只能通过 co_return 正确结束,普通 return 无法参与协程状态的清理流程
自动类型推导返回值(auto 或 concept) 编译器需要提前知道返回类型的 promise_type,否则无法构造协程框架
constexpr / consteval 函数 当前标准不支持在编译期求值协程逻辑
构造函数、析构函数 这些函数具有特殊的生命周期语义,与协程的延迟执行机制冲突
main 函数 标准明确规定 main 不能是协程,因为它是程序入口点,必须立即执行完毕

示例错误:

auto f() { co_return 1; } // 错误:不能使用 auto 推导协程返回类型


2. 协程的执行模型

每个协程背后都关联着一组内部结构,共同构成其执行环境。

2.1 协程的核心组件

组件 描述
Promise 对象 存储协程的结果或异常,由协程内部访问(类似 promise/future 中的 promise)
Coroutine Handle(协程句柄) 外部控制协程的非拥有式句柄,可用于 resume 或 destroy
Coroutine State(协程状态) 动态分配的对象,包含:
• Promise 对象
• 参数副本
• 局部变量和临时对象(跨越挂起点)
• 当前挂起点信息

这些组件共同构成协程的“运行上下文”,通常在堆上分配,除非被优化掉。

2.2 协程启动流程

当协程首次被调用时,编译器会自动生成一系列初始化步骤:

  1. 分配协程状态对象
    • 使用 operator new 在堆上分配足够大的内存块,容纳所有必要数据。
    • 分配大小取决于 sizeof(Promise) + 参数总大小 + 跨越挂起点的局部变量大小。
  2. 复制参数
    • 值传递的参数会被拷贝或移动到协程状态中。
    • 引用参数仍保持为引用——这意味着如果所引用的对象在其生命周期结束后协程才恢复,则会导致悬空引用(dangling reference),属于未定义行为。
  3. 构造 Promise 对象
    • 如果 PromiseType 提供了一个接受所有协程参数的构造函数,则调用该构造函数。
    • 否则调用默认构造函数。
  4. 调用 promise.get_return_object()
    • 获取一个代表协程的返回对象(如 task<T>generator<T>)。
    • 该对象会立即返回给调用者,即使协程尚未真正开始运行。
  5. 调用 promise.initial_suspend()co_await 其结果
    • 决定协程是否应在启动后立即挂起:
      • 返回 std::suspend_always{} → 协程挂起,等待手动 resume
      • 返回 std::suspend_never{} → 协程立即进入函数体执行
    • 这一步决定了协程是“懒启动”还是“急启动”。
  6. 进入协程体执行
    • 开始执行用户编写的函数逻辑。

所有这些步骤中的异常都会直接传播回调用者,而不是封装进 Promise 对象。

2.3 悬挂点

协程可以在多个位置暂停执行,称为“悬挂点”。常见的悬挂点包括:

  • co_await expr
  • co_yield expr
  • co_return expr
  • 函数入口(由 initial_suspend 控制)
  • 函数末尾(由 final_suspend 控制)

每次挂起时,当前局部变量和执行位置都会保留在协程状态中。


3. 协程的终止

协程可以通过正常返回或异常终止两种方式结束。

3.1 正常结束:co_return

当协程执行到 co_return 语句时,按照以下顺序进行清理:

  1. 调用对应的 Promise 方法
    • 若无返回值或返回 void 类型表达式,调用 promise.return_void()
    • 若有非 void 表达式,调用 promise.return_value(expr)
  2. 销毁局部变量
    • 按照声明的逆序销毁所有具有自动存储期的局部变量。
  3. 调用 promise.final_suspend()co_await 其结果
    • 通常用于通知外部协程已完成,或触发后续操作(如唤醒 continuation)。
    • initial_suspend 类似,可返回 suspend_alwayssuspend_never
  4. 释放协程状态
    • 调用 operator delete 释放堆上的协程状态内存。

co_return 的语义映射

co_return 形式 调用的 Promise 方法
co_return; promise.return_void()
co_return expr;(expr 类型为 void) promise.return_void()
co_return expr;(expr 非 void) promise.return_value(expr)

注意事项

若函数体自然“掉出结尾”而没有显式的 co_return,其效果等同于 co_return;。但如果 Promise 类型未定义 return_void(),则程序为 ill-formed(不符合语法规范)。这是 CWG 2556 缺陷报告修复的内容。

task<void> f() {
    // 错误!没有 co_return,也没有 co_await
    // 如果 Promise 没有 return_void,程序将无法通过编译
}

3.2 异常终止

如果协程抛出了未被捕获的异常,处理流程如下:

  1. 在协程内部捕获异常。
  2. 调用 promise.unhandled_exception(),通常在此方法中保存异常指针(std::current_exception())。
  3. 调用 promise.final_suspend()co_await 其结果。
  4. 禁止从此点恢复协程,否则行为未定义。

这保证了即使发生异常,协程也能安全地完成清理工作。


4. 动态内存分配与优化

4.1 默认行为

默认情况下,协程状态通过全局 operator new(std::size_t) 在堆上动态分配。释放则使用匹配的 operator delete

分配大小由编译器计算,至少包括:

  • Promise 对象
  • 所有按值传递的参数
  • 所有跨越挂起点的局部变量和临时对象

4.2 自定义分配器

可以通过在 PromiseType 中重载 operator newoperator delete 来指定自定义内存管理策略:

struct MyPromise {
    static void* operator new(std::size_t n) {
        return std::malloc(n);
    }

    static void operator delete(void* p, std::size_t n) {
        std::free(p);
    }
};

更进一步,支持“前置分配器约定”(leading allocator convention):

void* operator new(std::size_t n, MyAllocator& alloc, int extra_arg);

只要签名匹配,编译器会在调用 new 时自动传入请求大小及协程参数,便于实现上下文感知的内存池。

4.3 分配失败处理

如果 operator new 抛出异常,默认行为是传播 std::bad_alloc

但可通过定义静态成员函数 get_return_object_on_allocation_failure() 来提供回退机制:

struct MyPromise {
    static MyTask get_return_object_on_allocation_failure() {
        return MyTask(nullptr); // 返回无效 task
    }

    void* operator new(std::size_t n) noexcept {
        return malloc(n); // 返回 nullptr 表示失败
    }
};

此时使用 nothrow 版本的 new,失败时不抛异常,而是直接返回 fallback 对象。

4.4 分配优化:栈内嵌入

在满足以下两个条件时,协程状态可被优化至调用者的栈帧中(无需堆分配):

  1. 协程的生命周期严格嵌套于调用者之内(即不会逃逸)
  2. 协程帧的大小在调用点已知

例如:

void caller() {
    auto gen = fibonacci_sequence(10); // 可能内联到 caller 栈上
    for (auto v : gen) { /* ... */ }
}

这种优化极大地提升了性能,减少了内存分配开销和缓存 misses。


5. Promise 类型详解

5.1 Promise 的作用

Promise 是协程机制的核心纽带,负责协调协程内外的行为。其主要职责包括:

  • 创建返回对象(get_return_object
  • 控制初始挂起行为(initial_suspend
  • 控制最终挂起行为(final_suspend
  • 处理返回值(return_value / return_void
  • 处理未捕获异常(unhandled_exception
  • 自定义 co_await 行为(await_transform

每个协程都有一个与之关联的 Promise 实例,位于协程状态内部。

5.2 Promise 类型的确定规则

Promise 类型由模板 std::coroutine_traits<R, Args...>::promise_type 决定,具体规则如下:

  • 对于普通函数:std::coroutine_traits<R, Args...>::promise_type
  • 对于非静态成员函数:
    • 若为左值限定(非 rvalue-reference-qualified),则为 std::coroutine_traits<R, cv ClassT&, Args...>::promise_type
    • 若为右值限定,则为 std::coroutine_traits<R, cv ClassT&&, Args...>::promise_type

示例对照表

协程定义 对应的 Promise 类型
task<void> foo(int x); std::coroutine_traits<task<void>, int>::promise_type
task<void> Bar::foo(int x) const; std::coroutine_traits<task<void>, const Bar&, int>::promise_type
task<void> Bar::foo(int x) &&; std::coroutine_traits<task<void>, Bar&&, int>::promise_type

用户必须确保返回类型(如 task<T>)提供了正确的嵌套* promise_type,否则编译失败。


6. co_await 表达式详解

co_await expr 是协程暂停与恢复的核心机制,其实现依赖于 Awaitable 和 Awaiter 协议。

名词 含义
Awaitable 任何可以出现在 co_await 后面的表达式所对应的类型。例如:std::suspend_always、自定义任务类等。
Awaiter 实际执行挂起逻辑的对象。它是从 Awaitable 转换而来的,必须提供三个特定成员函数:await_readyawait_suspendawait_resume

6.1 执行流程

  1. 转换为 Awaitable
    • 若在初始/最终挂起或 yield 上下文中,直接使用 expr
    • 否则尝试调用 promise.await_transform(expr)
    • 否则使用 expr 本身
  2. 获取 Awaiter 对象
    • 查找operator co_await
      • 成员函数:expr.operator co_await()
      • 非成员函数:operator co_await(expr)
    • 若未找到,则 awaiter 就是 awaitable 本身
    • 若重载歧义 → 编译错误
  3. 判断是否需要挂起
    • 调用 awaiter.await_ready()
    • 返回 false 表示需要挂起
  4. 执行挂起
    • 调用 awaiter.await_suspend(handle)
    • 参数为当前协程的 coroutine_handle
    • 返回类型决定后续动作:
      • void:永久挂起,控制权交还调用者
      • booltrue 挂起,false 继续执行
      • coroutine_handle:立即 resume 另一个协程(可用于链式调度)
  5. 恢复后调用 await_resume()
    • 无论是否真正挂起,都会调用此函数
    • 返回值即为 co_await expr 的求值结果

恢复点位于 await_resume() 调用之前。

6.2 标准 Awaitables

C++20 提供两个简单 awaitable:

类型 行为
std::suspend_always 总是挂起
std::suspend_never 从不挂起

它们的作用是为协程提供最基础、最常用的挂起控制行为。它们被设计得非常轻量且语义清晰,主要用于实现协程的 初始挂起(initial_suspend最终挂起(final_suspend 行为。

std::suspend_always{};  // 常用于 initial_suspend 或 final_suspend
std::suspend_never{};   // 让协程立即运行

示例:切换线程的 Awaiter

auto switch_to_new_thread(std::jthread& out) {
    struct awaitable {
        std::jthread* p_out;

        bool await_ready() { return false; }

        void await_suspend(std::coroutine_handle<> h) {
            std::jthread& out = *p_out;
            if (out.joinable())
                throw std::runtime_error("Already joined");
            out = std::jthread([h] { h.resume(); });
            std::cout << "New thread ID: " << out.get_id() << '\n';
        }

        void await_resume() {}
    };
    return awaitable{&out};
}

task resuming_on_new_thread(std::jthread& out) {
    std::cout << "Start on: " << std::this_thread::get_id() << '\n';
    co_await switch_to_new_thread(out);
    std::cout << "Resume on: " << std::this_thread::get_id() << '\n';
}

输出可能为:

Start on: 139972277602112
New thread ID: 139972267284224
Resume on: 139972267284224

注意:await_suspend 中不要访问 *this,因为 awaiter 可能在其他线程被销毁。

6.3 await_transform:自定义 await 行为

PromiseType 可以定义 await_transform 来拦截所有 co_await 表达式,实现统一包装或日志等功能。

示例:可控挂起的协程

struct tunable_coro {
    class tunable_awaiter {
        bool ready_;
    public:
        explicit(false) tunable_awaiter(bool ready) : ready_(ready) {}
        bool await_ready() const noexcept { return ready_; }
        static void await_suspend(std::coroutine_handle<>) noexcept {}
        static void await_resume() noexcept {}
    };

    struct promise_type {
        bool ready_{true};

        auto get_return_object() { /* ... */ }
        static auto initial_suspend() { return std::suspend_always(); }
        static auto final_suspend() noexcept { return std::suspend_always(); }
        static void return_void() {}
        static void unhandled_exception() { std::terminate(); }

        // 拦截所有 co_await
        auto await_transform(std::suspend_always) {
            return tunable_awaiter(!ready_);
        }

        void disable_suspension() { ready_ = false; }
    };

    // ...
};

tunable_coro generate(int n) {
    for (int i = 0; i < n; ++i) {
        std::cout << i << ' ';
        co_await std::suspend_always{}; // 实际变为 tunable_awaiter
    }
}

调用示例:

auto coro = generate(8);
coro(); // 输出 0
coro(); // 输出 1
// ...
coro.disable_suspension(); // 关闭挂起
coro(); // 输出剩余全部数字(5 6 7)

输出:

0 1 : 2 : 3 : 4 : 5 6 7


7. co_yield:生成器模式

co_yield 用于实现 生成器模式(Generator Pattern)。它允许函数在执行过程中“产生”一个值并暂停,之后可以从中断处继续运行,非常适合用来实现惰性求值的序列,比如无限数列、数据流、遍历器等。

co_yield expr

相当于:

co_await promise.yield_value(expr)

它先将值传递给 yield_value,然后挂起协程并将值返回给消费者。

典型的 yield_value 实现会将值存储在 Promise 中,并返回 std::suspend_always,以便将控制权交还给调用者。

实现一个通用 Generator

template<typename T>
struct Generator {
    struct promise_type;
    using handle_type = std::coroutine_handle<promise_type>;

    struct promise_type {
        T value_;
        std::exception_ptr exception_;

        Generator get_return_object() {
            return Generator{handle_type::from_promise(*this)};
        }

        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }

        template<std::convertible_to<T> From>
        std::suspend_always yield_value(From&& from) {
            value_ = std::forward<From>(from);
            return {};
        }

        void return_void() {}
        void unhandled_exception() { exception_ = std::current_exception(); }
    };

    handle_type h_;

    explicit operator bool() { /* 检查是否还有下一个值 */ }
    T operator()() { /* 获取当前值并推进 */ }
    ~Generator() { if (h_) h_.destroy(); }
};

使用示例:斐波那契数列

Generator<uint64_t> fibonacci_sequence(unsigned n) {
    if (n == 0) co_return;
    co_yield 0;
    if (n == 1) co_return;
    co_yield 1;

    uint64_t a = 0, b = 1;
    for (unsigned i = 2; i < n; ++i) {
        uint64_t s = a + b;
        co_yield s;
        a = b; b = s;
    }
}

int main() {
    auto gen = fibonacci_sequence(10);
    for (int i = 0; gen; ++i)
        std::cout << "fib(" << i << ")=" << gen() << '\n';
}

输出:

fib(0)=0
fib(1)=1
...
fib(9)=34

C++23 已标准化 std::generator,见 <generator>

8. 其他

8.1 标准库支持

C++20 引入了 <coroutine> 头文件,提供基础类型支持:

类型 说明
std::coroutine_handle<P> 协程句柄,用于 resume/destroy
std::coroutine_traits<R, Args...> 元编程工具,提取 Promise 类型
std::suspend_always 总是挂起的 awaitable
std::suspend_never 从不挂起的 awaitable

C++23 新增:

类型 说明
std::generator<T> 同步生成器,替代手写 generator

8.2 参数与引用的安全问题(Dangling Reference)

由于协程可能延迟执行,若其捕获了局部对象的引用,而该对象已销毁,就会导致未定义行为。

示例:危险的悬空引用

struct S {
    int i;
    coroutine f() {
        std::cout << i; // 使用 i
        co_return;
    }
};

void bad1() {
    coroutine h = S{0}.f(); // S 临时对象立即销毁
    h.resume();             // UB:访问已释放的 S::i
    h.destroy();
}

正确做法:传值或延长生命周期

void good() {
    coroutine h = [](int i) -> coroutine { // i 以值方式传入
        std::cout << i;
        co_return;
    }(0); // i 被复制进协程帧
    h.resume(); // 安全
    h.destroy();
}

建议:尽量使用值传递参数,避免引用捕获短暂生存期的对象。


9. 线程完整使用示例 – 异步加法任务

我们实现一个简单的 task<int> 类型,表示一个可以异步计算并返回整数结果的任务。
主函数中调用一个协程 add_async(3, 4),它模拟延迟计算 3 + 4,并通过 co_await 获取结果。

该示例展示了:

  • 如何定义可等待的协程返回类型
  • Promise 的基本实现
  • co_returnco_await 的使用
  • 异常安全与生命周期管理
#include <iostream>
#include <coroutine>
#include <exception>

// ========================================
// 1. 定义 task<int>:代表一个能返回 int 的异步任务
// ========================================

struct task {
    struct promise_type;  // 声明嵌套的 promise_type
    using handle_type = std::coroutine_handle<promise_type>;

    handle_type h_;  // 存储协程句柄

    explicit task(handle_type h) : h_(h) {}

    ~task() {
        if (h_) {
            h_.destroy();  // 销毁协程状态
        }
    }

    task(const task&) = delete;
    task& operator=(const task&) = delete;

    task(task&& other) noexcept : h_(other.h_) {
        other.h_ = nullptr;
    }

    task& operator=(task&& other) noexcept {
        if (this != &other) {
            if (h_) h_.destroy();
            h_ = other.h_;
            other.h_ = nullptr;
        }
        return *this;
    }

    // 阻塞等待结果(用于演示,实际可用回调或事件循环)
    int get() {
        while (!h_.done()) {
            h_();  // 恢复协程执行
        }
        return h_.promise().result_;  // 从 promise 中取出结果
    }
};

// ========================================
// 2. 实现 promise_type
// ========================================

struct task::promise_type {
    int result_;                    // 存放 co_return 的值
    std::exception_ptr ex_ptr_;     // 存放抛出的异常

    // 创建返回对象(即 task 实例)
    task get_return_object() {
        return task{handle_type::from_promise(*this)};
    }

    // 初始不挂起 → 立即开始执行
    std::suspend_never initial_suspend() {
        return {};
    }

    // 最终挂起 → 让外部有机会获取结果后再销毁
    std::suspend_always final_suspend() noexcept {
        return {};
    }

    // 处理 co_return value
    void return_value(int value) {
        result_ = value;
    }

    // 处理未捕获异常
    void unhandled_exception() {
        ex_ptr_ = std::current_exception();
    }
};

// ========================================
// 3. 定义一个简单的协程:异步加法
// ========================================

task add_async(int a, int b) {
    std::cout << "正在计算 " << a << " + " << b << "...\n";

    // 模拟一些耗时操作(如 I/O 或网络请求)
    // 注意:这里只是 sleep,真实场景应结合非阻塞机制
    std::this_thread::sleep_for(std::chrono::milliseconds(500));

    // 返回结果
    co_return a + b;
}

// ========================================
// 4. 主函数:启动并运行协程
// ========================================

int main() {
    std::cout << "=== 开始协程测试 ===\n";

    // 启动协程
    auto t = add_async(3, 4);

    // 等待并获取结果
    int result = t.get();

    std::cout << "计算完成,结果是: " << result << "\n";

    std::cout << "=== 程序结束 ===\n";
    return 0;
}

可能的输出

=== 开始协程测试 ===
正在计算 3 + 4...
计算完成,结果是: 7
=== 程序结束 ===

关键点说明

组件 作用
task 用户定义的协程返回类型,封装协程句柄
promise_type 控制协程行为的核心类,决定如何创建返回对象、是否挂起、如何返回值等
get_return_object() 创建并返回 task 对象给调用者
initial_suspend() 返回 suspend_never 表示立即执行协程体
final_suspend() 返回 suspend_always 以便外部读取结果后才销毁
return_value() 接收 co_return 的值并保存到 promise
co_return 结束协程并将值传给 return_value
co_await 在本例中隐式用于 initial_suspendfinal_suspend,显式可用于更复杂的 awaitable
get() 方法 驱动协程运行直到完成,并提取结果
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇