C++20协程概念以及实现

C++20 协程是一种可暂停、可恢复 的函数机制。编译器会将协程转换为状态机,并通过 Coroutine Frame 保存执行上下文。Promise 负责管理协程生命周期和返回值,Handle 负责恢复和销毁协程,Awaiter 负责实现 co_await 的等待逻辑。协程属于用户态调度,切换成本远低于线程,但默认不能实现真正的并行执行。

一、核心概念

概念 定义
协程(Coroutine) 可以暂停执行并在之后恢复的函数
协程帧(Coroutine Frame) 堆上分配的内存,保存协程的局部变量、参数、执行位置等状态
有栈协程 每个协程独立栈空间(如 Go goroutine),切换需换栈指针
无栈协程(C++20采用) 共享栈,通过状态机切换,仅修改几个状态变量,切换成本极低
协作式调度 协程主动让出执行权,非抢占式

二、三个关键字

关键字 含义 作用
co_await 等待 暂停协程,等待某个可等待对象完成后恢复
co_yield 产出/让出 产生一个值给调用者,然后挂起协程
co_return 协程返回 结束协程,可带返回值

规则 :函数中包含任一关键字,即被识别为协程函数 ,返回类型必须包含 promise_type

三、核心组件(三大件)

组件 作用 本质
Promise Type 控制协程生命周期和返回值 协程的"管家",定义在返回类型的嵌套类中
Coroutine Handle 恢复/销毁协程的句柄 std::coroutine_handle<promise_type>,协程的"遥控器"
Awaiter 定义 co_await 的等待行为 实现 await_ready()await_suspend()await_resume() 的对象

四、Promise Type 详解

4.1 必须实现的接口(5个)

函数 是否必须 调用时机 作用
get_return_object() 协程创建时 构造并返回协程对象给调用者
initial_suspend() 协程函数体执行前 控制协程是立即执行还是立即挂起
final_suspend() 协程函数体执行完毕后 控制协程结束时的行为
unhandled_exception() 协程内发生未捕获异常时 保存异常信息
return_void() / return_value(T) ✅ 二选一 执行 co_return 处理返回值

4.2 可选接口

函数 作用
yield_value(T) 使用 co_yield 时必须实现
operator new(size_t) 自定义协程帧内存分配
operator delete(void*) 自定义协程帧内存释放

4.3 各函数详解

get_return_object()
cpp 复制代码
Task get_return_object() {
    return Task{
        std::coroutine_handle<promise_type>::from_promise(*this)
    };
}
  • 作用:给调用者返回一个"遥控器"(包含 handle 的对象)

  • 关键 :用 from_promise(*this) 将 promise 和 handle 关联

initial_suspend()
cpp 复制代码
std::suspend_always initial_suspend() noexcept { return {}; }  // 挂起(惰性启动)
std::suspend_never initial_suspend() noexcept { return {}; }   // 不挂起(急切启动)
  • 作用:决定协程创建后是立即执行还是先挂起

  • 常用suspend_always(惰性启动,最常用)

final_suspend()
cpp 复制代码
std::suspend_always final_suspend() noexcept { return {}; }  // 挂起(推荐)
std::suspend_never final_suspend() noexcept { return {}; }     // 不挂起(危险)
  • 作用:决定协程结束后是立即销毁还是先挂起

  • ⚠️ 必须标记 noexcept

  • 推荐suspend_always,让调用者读取完结果再手动 destroy()

unhandled_exception()
cpp 复制代码
void unhandled_exception() {
    exception_ = std::current_exception();  // 保存异常
}
  • 作用:捕获协程内未处理的异常

  • 标准做法 :用 std::current_exception() 保存,稍后 rethrow_exception() 重新抛出

return_void() / return_value(T)
cpp 复制代码
// 无返回值协程:co_return;
void return_void() noexcept {}

// 有返回值协程:co_return 42;
void return_value(int value) {
    value_ = value;  // 保存到 promise 成员变量
}
  • 二选一:不能同时存在

  • 作用 :将 co_return 的值保存到 promise 中,供外部读取


五、Coroutine Handle(协程句柄)

cpp 复制代码
std::coroutine_handle<promise_type> handle;
方法 作用
handle.resume() 恢复协程执行
handle.done() 检查协程函数体是否执行完毕
handle.destroy() 销毁协程帧,释放堆内存
handle.promise() 获取关联的 promise 对象

注意done() == true 不代表协程帧已销毁,只是函数体执行完了。


六、Awaiter(可等待对象)

co_await 后面跟的对象必须实现三个接口:

接口 调用时机 返回值含义
bool await_ready() 挂起前检查 true = 不挂起直接继续;false = 需要挂起
void/bool/coroutine_handle await_suspend(handle) 决定挂起时的操作 void = 挂起; bool = true挂起/false不挂起; handle = 挂起当前并恢复目标
T await_resume() 恢复后调用 返回值作为 co_await 表达式的结果

标准库提供的两个 Awaiter

cpp 复制代码
std::suspend_always{};  // await_ready 始终返回 false(总是挂起)
std::suspend_never{};   // await_ready 始终返回 true(从不挂起)

七、协程生命周期(执行流程)

复制代码
调用协程函数
    │
    ▼
┌─────────────────┐
│ 1. 堆上分配协程帧 │  ← 编译器自动
│ 2. 构造 promise  │
│ 3. get_return_object() │  ← 返回 Task 给调用者
└─────────────────┘
    │
    ▼
┌─────────────────┐
│ 4. initial_suspend() │  ← 决定:挂起 or 继续
└─────────────────┘
    │
    ├── 返回 suspend_always ──→ 协程挂起,控制权回 main()
    │                              │
    │                              ▼
    │                         main() 继续执行
    │                              │
    │                              ▼
    │                         调用 task.resume()
    │                              │
    │                              ▼
    │                         协程函数体开始/继续执行
    │                              │
    │                         遇到 co_await
    │                              │
    │                         ┌─────────────┐
    │                         │ await_ready() │──→ true:不挂起,继续
    │                         └─────────────┘    false:挂起,回 main()
    │                              │
    │                         再次 resume...
    │                              │
    │                              ▼
    │                         遇到 co_return
    │                              │
    │                              ▼
    │                         return_value() / return_void()
    │                              │
    │                              ▼
    │                         final_suspend()  ← 决定结束行为
    │                              │
    │                         ┌─────────────┐
    │                         │suspend_always│──→ 挂起,状态保留
    │                         │  (推荐)      │    需手动 destroy()
    │                         └─────────────┘
    │                              │
    │                         ┌─────────────┐
    │                         │suspend_never  │──→ 协程帧立即销毁
    │                         │  (危险)      │    不能再访问数据
    │                         └─────────────┘
    │
    └── 返回 suspend_never ──→ 协程体立即执行
                                   │
                              (后续流程同上)

八、co_yield 的本质

co_yield value 是语法糖,等价于:

cpp 复制代码
co_await promise.yield_value(value);

执行流程

  1. 调用 promise.yield_value(value) --- 将值保存到 promise

  2. yield_value 返回一个 Awaiter(通常是 suspend_always

  3. 执行 co_await 这个 Awaiter --- 挂起协程


九、协程 vs 线程

对比项 线程 C++20 协程
调度者 操作系统内核 用户程序
切换成本 ~1-2μs(内核态切换) ~10-20ns(用户态)
内存占用 独立栈 1-8MB 堆上按需分配,几十~几百字节
并发性 真正并行(多核同时执行) 单线程内协作式,同一时刻只有一个执行
阻塞影响 阻塞线程浪费 OS 资源 阻塞只影响自己,线程可执行其他协程
适用场景 CPU 密集型 IO 密集型、高并发网络服务

十、C++20 协程的特点

特点 说明
编译器基础设施 标准只提供底层机制,没有现成 Task/Future
无栈协程 只能在协程函数顶层挂起,不能在普通子函数内挂起
需要自行封装 实际项目通常使用 async_simple、cppcoro 等库
协程传染性 一旦使用协程,调用链上相关函数通常也需要改为协程风格

十一、常见陷阱

陷阱 后果 避免方法
忘记 destroy() 内存泄漏 在 Task 析构函数中调用 handle.destroy()
final_suspend 返回 suspend_never 悬空引用,UB 返回 suspend_always,让调用者控制销毁
协程内使用 this 指针后挂起 恢复时 this 可能悬空 使用 std::shared_ptr 或避免
未处理异常 std::terminate unhandled_exception() 中保存异常
并发调用 resume() 未定义行为 确保同一时刻只有一个线程 resume

十二、最简完整代码模板

cpp 复制代码
#include <iostream>
#include <coroutine>

// ========== 1. 定义返回类型(包含 promise_type)==========
struct Task {
    struct promise_type {
        // 返回值存储
        int value_ = 0;
        std::exception_ptr exception_;
        
        // ① 返回对象
        Task get_return_object() {
            return Task{
                std::coroutine_handle<promise_type>::from_promise(*this)
            };
        }
        
        // ② 启动时挂起(惰性)
        std::suspend_always initial_suspend() noexcept {
            return {};
        }
        
        // ③ 结束时挂起(让调用者收尾)
        std::suspend_always final_suspend() noexcept {
            return {};
        }
        
        // ④ 异常处理
        void unhandled_exception() {
            exception_ = std::current_exception();
        }
        
        // ⑤ 处理 co_return 值
        void return_value(int v) {
            value_ = v;
        }
    };
    
    // 协程句柄(遥控器)
    std::coroutine_handle<promise_type> handle;
    
    // 便捷方法
    void resume() { handle.resume(); }
    bool done() { return handle.done(); }
    int result() { return handle.promise().value_; }
    
    // RAII 销毁
    ~Task() { if (handle) handle.destroy(); }
};

// ========== 2. 协程函数 ==========
Task my_coro() {
    std::cout << "协程执行中..." << std::endl;
    co_await std::suspend_always{};  // 挂起
    std::cout << "协程恢复..." << std::endl;
    co_return 42;  // 返回值
}

// ========== 3. 使用 ==========
int main() {
    auto task = my_coro();  // 创建,执行到 initial_suspend 挂起
    task.resume();          // 执行到 co_await 挂起
    task.resume();          // 执行到 final_suspend 挂起
    
    std::cout << "结果: " << task.result() << std::endl;  // 42
    return 0;  // task 析构,调用 destroy()
}

十三、记忆口诀

三关键字co_await 等一等,co_yield 给一给,co_return 回一回

三大件:Promise 管生命周期,Handle 是遥控器,Awaiter 定义等待行为

五必写get_return_object 给遥控,initial_suspend 定启动,final_suspend 定结束,unhandled_exception 抓异常,return_void/value 存结果

生命周期:创建 → 给遥控 → 定启动 → (挂起等 resume)→ 执行 → 定结束 → 手动销毁