【C++现代#15】C++20 协程

📃个人主页:island1314
⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞
- 生活总是不会一帆风顺,前进的道路也不会永远一马平川,如何面对挫折影响人生走向 – 《人民日报》
🔥 目录
1. 基本概述
1.1 什么是协程
- 普通函数:调用 -> 执行到
return-> 销毁所有栈帧,返回调用者,一生只返回一次。 - 协程:调用 -> 执行到
co_await,co_yield,co_return-> 暂停(挂起),保存当前状态(局部变量、执行位置等) -> 之后某个时刻被恢复,从暂停点继续执行->最终执行到co_return结束。

1.2 C++20 协程
1️⃣ 要点 1:无栈协程 = 轻量但"传染式暂停"
意思是:“暂停时只保存协程帧(程序计数器、局部对象、Promise 对象等,保存在堆上),而不是整个调用栈”。
- 这使得它们非常轻量,但意味着在协程内暂停时,其调用者也会被暂停。
图解:有栈 vs 无栈
┌─────────────────────────────────┐
│ 🔸 有栈协程(如 Go/Java) │
│ • 每个协程独立分配栈空间(KB~MB)│
│ • 暂停时:保存整个栈 → 切换开销大│
│ • 优点:协程内可调用任意函数 │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ 🔸 C++20 无栈协程 │
│ • 协程帧 = 局部变量 + 状态 + promise │
│ • 帧分配在**堆上**(可自定义分配器)│
│ • 暂停时:仅保存"必要状态" → 切换≈函数调用 │
│ • 缺点:协程内暂停 → 调用链全部暂停 │
└─────────────────────────────────┘
- 有栈协程:就像有多个独立的办公桌。每个协程都有自己的完整办公桌(调用栈),上面放着正在处理的文件(局部变量、返回地址等)。当你从一个协程切换到另一个时,你是走到另一个完整的办公桌前继续工作,之前办公桌的所有状态都原封不动。
- 无栈协程:就像只有一个办公桌,但是有很多个待办事项清单。你同时处理多个任务,但每次只做当前任务的一小步,做完这一步后,就在清单上记录下这一步做到哪里了(状态),然后切换到下一个任务。任务的”状态"是显式记录在清单上的,而不是散落在整个办公桌上。
⚠️ "传染式暂停"的关键影响
task<void> inner() {
co_await async_io(); // 暂停点
}
task<void> middle() {
co_await inner(); // ❗ inner 暂停 → middle 也暂停
}
task<void> outer() {
co_await middle(); // ❗ 暂停"传染"到最外层
}
💡 设计启示:协程函数签名即"契约"——返回
task<T>意味着"可能异步",调用方必须co_await。
2️⃣ 要点 2:C++20 协程是"底层原语",不是"开箱即用"
C++20协程它本身不是一个开箱即用的并发工具,而是一个用于构建异步编程的强大底层语言设施,项目要使用协程,通常要使用一些上层封装抽象的协程库或者结合使用的库,如libunifex/cppcoro/Boost.Asio等等。
“它本身不是并发工具,而是用于构建异步编程的底层语言设施”
类比理解
| 层级 | C++ 协程 | 类比 |
|---|---|---|
| 🔹 语言层 | co_await / promise_type / coroutine_handle |
汇编指令 |
| 🔹 库层 | cppcoro::task / Asio::awaitable |
C 标准库函数 |
| 🔹 应用层 | 你的业务逻辑(HTTP 服务、游戏逻辑) | 应用程序 |
为什么标准库不直接提供 std::task?
- 异步模型多样:回调、future、sender/receiver、协程… 难以统一
- 调度策略灵活:单线程事件循环?线程池?GPU 协程?标准无法预设
- 零开销抽象:C++ 哲学——“不为不用的特性付费”,把选择权交给库作者
结论:学 C++20 协程,本质是学如何用语言原语构建自己的异步抽象。
3️⃣ 要点 3:三个关键字 + 状态机转换
“当函数包含 co_await/co_yield/co_return 任意一个关键字时,它就是协程”
co_await expression:用于暂停执行直到恢复co_yield expression:用于暂停执行并返回一个值co_return expression:用于完成执行并返回一个值
编译器做了什么?
// "同步风格"代码
task<int> compute() {
int x = 42;
co_await async_op(); // ← 暂停点
co_return x * 2;
}
// 编译器生成的"状态机"伪代码(简化)
struct compute_frame {
enum class State { Start, AfterAwait, Done };
State state = State::Start;
int x;
promise_type promise;
coroutine_handle<> caller;
void resume() {
switch (state) {
case Start:
x = 42;
// 注册 async_op 的回调,暂停
state = State::AfterAwait;
return;
case AfterAwait:
promise.return_value(x * 2); // co_return
state = State::Done;
return;
case Done: /* nothing */ break;
}
}
};
Promise 类型:协程的"控制中心"
struct TaskPromise {
// 协程开始:返回用户看到的对象
Task get_return_object();
// 初始暂停点:通常返回 suspend_always
std::suspend_always initial_suspend();
// 结束暂停点:决定协程结束后是否自动销毁
std::suspend_always final_suspend();
// co_return value 时调用
void return_value(int v);
void return_void();
// 异常处理
void unhandled_exception();
};
💡 关键:协程的行为(何时暂停、如何返回、异常处理)完全由
promise_type定义。
4️⃣ 要点 4:C++23/26 标准库演进
C++20协程学习曲线陡峭,且当前直接使用很麻烦困难,但是学习它对于我们使用和理解协程的运行机制和高效使用协程库有很大的帮助,“C++23 提供 std::generator,C++26 计划支持 std::task”,但C++26当前还没正式发布,我们需要静等后续C++标准库的完善和编译器的支持。
标准库协程支持时间线
C++20 (2020)
├─ ✅ 协程原语:coroutine_handle, promise_type, awaiter
├─ ❌ 无高层抽象:无 std::task / std::generator
└─ 📦 依赖第三方库:cppcoro, libunifex, Asio
C++23 (2023)
├─ ✅ std::generator<T>:生成器模式(co_yield 序列)
├─ ✅ std::expected<T,E>:配合协程的错误处理
└─ ❌ 仍无 std::task:异步计算需等 C++26
C++26 (预计 2026)
├─ 🔄 std::task<T> 提案中:通用异步任务抽象
├─ 🔄 std::async_scope:协程生命周期管理
└─ 🔄 与其他并发提案(executors)协同设计
C++23 std::generator 示例解析
#include <generator>
#include <iostream>
std::generator<long long> fibonacci(int count) {
long long a = 0, b = 1;
for (int i = 0; i < count; ++i) {
co_yield a; // ← 暂停,产出 a,等待下一次迭代
auto next = a + b;
a = b;
b = next;
}
}
int main() {
// 范围 for 循环自动调用 generator 的 begin()/end()
// 每次 ++it 触发 co_yield 恢复 → 拿到下一个值
for (auto num : fibonacci(10)) {
std::cout << num << " "; // 0 1 1 2 3 5 8 13 21 34
}
}
🔍 底层发生了什么?
fibonacci(10)返回generator<long long>对象- 范围 for 调用
gen.begin()→ 创建迭代器,内部持有 coroutine_handle - 每次迭代:
- 调用
iter.operator++()→handle.resume() - 协程执行到
co_yield a→ 暂停,返回 a - 调用
iter.operator*()→ 拿到 a 的值
- 调用
- 循环结束或协程 co_return → generator 析构,自动销毁协程帧
✅ 价值:
std::generator把"手动管理协程句柄"封装成"范围迭代器",用法极其简洁。
🎁 附:协程核心概念速查
// 🔹 三个关键字
co_await expr; // 暂停,等待 expr 就绪(异步 IO/定时器)
co_yield value; // 暂停并产出值(生成器/流式处理)
co_return value; // 结束协程并返回结果
// 🔹 协程帧 = 堆上分配的状态
struct coroutine_frame {
promise_type promise; // 控制中心
void* local_vars; // 局部变量(按需要捕获)
state_t state; // 当前暂停点
coroutine_handle<> caller; // 调用者句柄(用于恢复)
};
// 🔹 awaiter 三函数(co_await 的核心)
struct MyAwaiter {
bool await_ready() const noexcept; // 是否跳过暂停?
void await_suspend(coroutine_handle<> h); // 暂停时注册回调
ResultType await_resume(); // 恢复后返回结果
};
// 🔹 最佳实践口诀
// • 协程函数返回 task<T>,调用方必须 co_await
// • 避免在协程内调用阻塞 syscall
// • 大局部变量用 shared_ptr,减少协程帧大小
// • 用 when_all/when_any 组合协程,避免手动管理 handle
2. 进程 vs 线程 vs 协程
🏭 核心比喻:工厂模型(形象理解)
┌─────────────────────────────────────┐
│ 🏭 操作系统 = 工厂厂长 │
├─────────────────────────────────────┤
│ 👷 进程 = 独立车间 │
│ • 每个车间有独立空间、工具、原料 │
│ • 车间之间隔离,通信靠"传纸条" │
├─────────────────────────────────────┤
│ 🔧 线程 = 车间里的工人 │
│ • 同一车间的工人共享工具/原料 │
│ • 厂长(OS)调度工人到流水线(CPU)│
├─────────────────────────────────────┤
│ 🎭 协程 = 工人手中的"可暂停任务模具" │
│ • 工人(线程)拿着模具(协程)工作 │
│ • 任务卡住时换模具,不换工人 │
│ • 模具切换在工人手中完成(用户态) │
└─────────────────────────────────────┘
💡 关键认知:协程不是"更轻的线程",而是线程的任务调度策略。
三维度对比
| 维度 | 进程 (Process) | 线程 (Thread) | 协程 (Coroutine) |
|---|---|---|---|
| 所属层级 | 操作系统资源分配单位 | 操作系统调度单位 | 语言/库级执行单元 |
| 资源隔离 | ✅ 独立地址空间、文件描述符 | ❌ 共享进程资源 | ❌ 共享线程资源 |
| 切换主体 | 操作系统内核 | 操作系统内核 | 用户态代码/运行时 |
| 切换成本 | ⭐⭐⭐⭐⭐ 极高 (ms 级) | ⭐⭐⭐ 高 (μs 级) | ⭐ 极低 (ns 级) |
| 并发模型 | 多进程并行 | 多线程并行 | 单线程内协作式并发 |
| 阻塞行为 | 阻塞整个进程 | 阻塞整个线程 | 仅暂停当前协程 ✅ |
| 编程模型 | IPC (管道/共享内存) | 锁/条件变量/原子操作 | 同步写法 + co_await |
| 典型场景 | 浏览器标签、微服务 | Web 服务器、GUI 应用 | 高并发 IO、游戏逻辑、生成器 |
深度拆解:关键差异
1️⃣ 资源与隔离性
// 进程:完全隔离
pid_t pid = fork(); // 创建新进程,独立内存空间
// ✅ 一个进程崩溃不影响其他进程
// ❌ 通信复杂:pipe/socket/shared_memory
// 线程:共享内存
std::thread t1([](){ /* 访问全局变量 */ });
std::thread t2([](){ /* 访问同一全局变量 */ });
// ✅ 通信简单:直接读写共享内存
// ❌ 需手动同步:mutex/atomic,否则数据竞争
// 协程:共享线程栈(逻辑上)
task<void> co1() { /* 访问局部变量 */ }
task<void> co2() { /* 访问同一线程的全局变量 */ }
// ✅ 无锁编程:单线程内协程切换无并发竞争
// ❌ 协程内仍需用锁访问跨线程共享资源
2️⃣ 切换成本量化(实测参考)
| 切换类型 | 典型耗时 | 主要开销 |
|---|---|---|
| 进程切换 | 10~100 μs | TLB 刷新、页表切换、内核态 trap |
| 线程切换 | 1~10 μs | 寄存器保存、内核调度器介入 |
| 协程切换 | 10~100 ns | 仅保存少量寄存器 + 状态机跳转 |
📈 数量级差异:协程切换比线程快 100~1000 倍,这是高并发 IO 场景的核心优势。
3️⃣ 阻塞行为对比(关键!)
// 线程阻塞:整个线程挂起
void thread_func() {
recv(sock, buf, len, 0); // ❌ 阻塞:该线程无法执行其他任务
// 如果需要处理 10K 连接 → 需要 10K 线程 → 内存/调度爆炸
}
// 协程阻塞:仅当前协程暂停,线程继续执行其他协程
task<void> coroutine_func(AsyncSocket& sock) {
co_await sock.async_recv(buf); // ✅ 暂停:线程去执行其他协程
// 1 个线程可调度 10K+ 协程 → 资源利用率极大提升
}
4️⃣ 编程模型对比
| 模型 | 代码风格 | 调试难度 | 适用场景 |
|---|---|---|---|
| 多线程 + 回调 | 回调地狱,状态分散 | ⭐⭐⭐⭐⭐ | 早期 Node.js,不推荐 |
| 多线程 + 同步阻塞 | 线性写法,简单 | ⭐⭐ | 连接数 < 1K 的服务 |
| 协程 + 异步 IO | 线性写法 + 异步执行 ✨ | ⭐⭐⭐ | 高并发 IO、游戏服务器 ✅ |
// 协程风格:像写同步代码一样写异步逻辑
task<HttpResponse> handle_request(AsyncSocket& sock) {
auto req = co_await read_request(sock); // 暂停等待 IO
auto result = process(req); // CPU 计算(可 yield)
co_await write_response(sock, result); // 暂停等待 IO
co_return;
}
// 对比回调风格:嵌套 + 状态机手动管理,难以维护
协程与线程:协作而非替代
核心关系图
┌─────────────────────────────────┐
│ 🧵 线程池(N 个线程 = N 个工人) │
├─────────────────────────────────┤
│ 🎭 协程调度器(M 个协程 = M 任务)│
│ • M >> N(万级协程 / 几十线程)│
│ • 协程阻塞时 → 让出线程 │
│ • IO 就绪时 → 协程重回就绪队列 │
└─────────────────────────────────┘
最佳实践架构
// 1. 创建线程池(提供并行计算能力)
ThreadPool pool(std::thread::hardware_concurrency());
// 2. 每个线程运行一个协程调度器
pool.run([](Executor* exec) {
while (running) {
// 事件循环:调度就绪的协程
for (auto& co : ready_coroutines) {
co.resume(); // 用户态切换,极快
}
// 等待 IO 事件(epoll/kqueue/io_uring)
auto events = wait_for_io(timeout);
for (auto& e : events) {
e.coroutine_handle.resume(); // IO 就绪,恢复协程
}
}
});
// 3. 业务代码:用协程写高并发逻辑
task<void> handle_client(AsyncSocket sock) {
while (true) {
auto req = co_await sock.recv(); // 非阻塞,暂停协程
auto resp = compute(req); // CPU 密集?可 yield 给其他协程
co_await sock.send(resp);
}
}
为什么不是"协程替代线程"?
| 场景 | 协程优势 | 线程必要性 |
|---|---|---|
| CPU 密集计算 | ❌ 协程无法并行(单线程内) | ✅ 多核并行必须用多线程 |
| 阻塞系统调用 | ❌ 协程无法"穿透"阻塞 syscall | ✅ 需用线程池隔离阻塞 IO |
| 实时性要求 | ❌ 协程协作式,可能被长任务占用 | ✅ 线程抢占式,OS 保证调度公平 |
| 生态兼容 | ❌ 第三方库可能不支持协程 | ✅ 线程是 OS 原生,兼容性最好 |
✅ 结论:线程负责"并行",协程负责"并发",二者组合才是王道。
小结
| 概念 | 本质 | 关键优势 | 典型误用 |
|---|---|---|---|
| 进程 | OS 资源隔离单位 | 崩溃隔离、安全沙箱 | 用进程做高并发 IO(太重) |
| 线程 | OS 调度执行单位 | 多核并行、生态成熟 | 用 1 线程 1 连接扛 10K QPS(太贵) |
| 协程 | 用户态任务调度单元 | 高并发 IO、代码简洁 | 用协程做 CPU 密集计算(无加速) |
✨ 终极口诀:
进程管隔离,线程管并行,协程管并发;
IO 密集上协程,CPU 密集上线程,安全隔离上进程。
3. 协程的控制组件
3.1 Promise 对象
由编译器在协程帧内创建的对象,它是协程的大脑,控制着协程的行为,Promise类型必须提供以下核心接口,用来控制协程的行为。
① get_return_object()
- 作用:创建并返回给调用者协程的返回值。这是在协程函数体执行之前被调用的。
- 返回值类型:协程的返回类型(例如外层的Task、Generator)。
典型实现:通常会用 coroutine_handle<Promise>::from_promise(*this) 来创建一个指向当前Promise对象的句柄,并用这个句柄来构造外层的返回值对象。
② initial_suspend()
- 作用:决定协程在开始执行函数体之前是否应该先挂起。
- 返回值:一个满足
Awaitable概念的对象(通常是std::suspend_always或std::suspend_never) std::suspend_always{}:总是挂起。这意味着协程一创建就是惰性的,不会自动开始执行,需要手动resume(),这是“懒汉式”协程的常见选择。std::suspend_never{}:从不挂起。协程创建后会立即开始执行,直到遇到第一个挂起点。这是“饿汉式”协程的常见选择。
Generator get_return_object() {
return Generator{std::coroutine_handle<Promise>::from_promise(*this)};
}
③ final_suspend()
- 作用:决定协程在执行完函数体(或通过
co_return返回)后,在彻底销毁自身之前是否应该挂起。 - 返回值:同样是一个
Awaitable概念对象。(通常是std::suspend_always或std::suspend_never) - 重要提示:如果返回
std::suspend_always,你必须在协程外部某处手动调用destroy()来销毁协程帧,否则会发生内存泄漏。如果返回std::suspend_never,协程会在返回后自动清理自己。 - 典型用途:使用返回
std::suspend_always,在final_suspend()中挂起,可以让:你在协程外部有机会在其销毁前读取最终状态或结果。
④ return_void() 和 return_value(...)
- 作用:处理 co_return 语句。
- Promise类型必须实现这两个的其中之一,但不能同时实现。
void return_void():用于处理 co_return;(无返回值)void return_value(type value):用于处理 co_return value;(有返回值)。value 的类型由我们自行指定,通常会把它存储到Promise 的一个成员变量中,以便外部获取。
⑤ yield_value(...)
- 作用:处理 co_yield 语句。co_yield expression本质上被编译器翻译为
co_await promise.yield_value(expression) - 返回值:一个Awaitable 概念对象,用于决定在产出值之后是否要挂起协程(通常返回
std::suspend_always{}来挂起,让调用者有机会处理产出的值) - 典型实现:接收产出的值,将其存储到Promise类型的一个成员变量中,然后返回一个挂起指示器
std::suspend_always yield_value(int value) {
current_value = value; // 存储值到current_value成员变量,供外部访问
return {};
}
⑥ unhandled_exception()
- 作用:当协程体内发生异常,并且该异常未被协程体内的try/catch块捕获时,这个异常会被自动传递给
unhandled_exception()方法。 unhandled_exception():Promise类型的核心异常处理函数,协程内任何未捕获的异常都会调用此函数,std::current_exception()捕获当前异常并转换为std::exception_ptr,std::rethrow_exception()重新抛出存储的异常- RAll模式在协程中依然有效,即使协程抛出异常,栈上的对象也会正确析构,协程帧本身的内存由
coroutine_handle管理
#include <coroutine>
#include <iostream>
#include <stdexcept>
// 最简单的协程类型
struct SimpleTask {
struct promise_type {
int value = 0;
std::exception_ptr exception; // 存储异常的地⽅
SimpleTask get_return_object() {
return SimpleTask{
std::coroutine_handle<promise_type>::from_promise(*this) };
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
// 关键:异常处理函数
void unhandled_exception() {
std::cout << "捕获到异常,存储起来\n";
exception = std::current_exception(); // 捕获异常
}
void return_value(int val) {
value = val;
}
};
std::coroutine_handle<promise_type> handle;
// 获取结果,如果有异常就抛出
int get_result() {
if (!handle.done()) handle.resume();
if (handle.promise().exception) {
std::cout << "重新抛出存储的异常\n";
std::rethrow_exception(handle.promise().exception);
}
return handle.promise().value;
}
~SimpleTask() {
if (handle) handle.destroy();
}
};
// 会抛出异常的协程函数
SimpleTask throwing_coroutine() {
std::cout << "异常协程开始执行\n";
co_await std::suspend_always{};
std::cout << "准备抛出异常...\n";
throw std::runtime_error("协程内部发⽣错误!"); // 抛出异常
co_return -1; // 这⾏不会执⾏
}
// 立即抛出异常的协程
SimpleTask immediate_throw_coroutine() {
std::cout << "⽴即抛出异常的协程\n";
throw std::logic_error("⽴即发⽣的错误!");
co_return 100; // 不会执⾏
}
int main() {
std::cout << "=== 测试异常协程 ===\n";
try {
auto task2 = throwing_coroutine();
int result = task2.get_result(); // 这⾥会抛出异常
std::cout << "结果: " << result << "\n\n";
}catch (const std::runtime_error& e) {
std::cout << "捕获到 runtime_error: " << e.what() << "\n\n";
}
std::cout << "=== 测试立即异常 ===\n";
try {
auto task3 = immediate_throw_coroutine();
int result = task3.get_result(); // 这⾥会抛出异常
std::cout << "结果: " << result << "\n";
}catch (const std::logic_error& e) {
std::cout << "捕获到 logic_error: " << e.what() << "\n";
}
return 0;
}
⑦ await_transform
await_transform是Promise类型的一个可选成员函数,它允许在co_await表达式上应用自定义转换。当编译器遇到co_await expression时,会查找Promise类型的await_transform(type expression)方法,找不到就编译报错,本质上就是支持把一个expression变成Awaitable对象。- 返回值:一个 Awaitable 概念对象
struct FileReadAwaiter {
int fd_;
FileReadAwaiter(int fd) : fd_(fd) {}
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
// ... 使用异步 I/O 库(如 io_uring, libuv)来异步读取 fd_
// 当读取完成时,恢复协程 h
async_read(fd_, h);
}
std::string await_resume() {
// ... 返回读取到的数据
return read_buffer;
}
};
struct MyPromise {
// ... 其他 Promise 成员函数同上
// 当协程内 co_await ⼀个 int 时,我们将其视为文件描述符
// 并返回⼀个自定义的 FileReadAwaiter
auto await_transform(int fd) {
std::cout << "Interpreting int as file descriptor for async read\n";
return FileReadAwaiter{fd};
}
};
MyCoroutine my_coroutine() {
int socket_fd = open_socket(); // 假设返回⼀个文件描述符
std::string data = co_await socket_fd; // 神奇地变成了异步读取!
std::cout << "Received: " << data << std::endl;
}
3.2 协程句柄
协程句柄是一个 非拥有的、轻量级 的句柄,它代表并允许你从外部直接操作一个特定的 协程帧,它是你与协程运行时交互的主要接口,你可以把它想象成一个指向特定协程的遥控器,有点类似于系统API的线程句柄,文件句柄,或者理解成类似指向 Promise 对象的智能指针,但是功能跟智能指针就不搭边了,智能指针的核心是要控制资源的释放,这里是要管理 Promise 对象。
通常我们在协程返回类型中定义
std::coroutine_handle<Promise> handle的句柄成员变量。
协程句柄的核心操作API主要有以下几个,更多的API请看文档:[https://en.cppreference.com/w/cpp/coroutine/coroutine_handle.html]
① from_promise()/promise()
static coroutine_handle from_promise(Promise& p );,用于使用一个Promise对象创建一个指向Promise对象的句柄,通常在 Promise 类型的get_return_object成员函数中使用。Promise& promise()const;方便我们使用句柄可以获取管理的Promise对象。
② resume()/operator()
- 作用:让一个处于挂起状态的协程继续执行。
- 行为:调用后,协程从上次挂起的地方开始执行,直到遇到下一个挂起点、结束或抛出异常。C.注意:不能在非挂起状态的协程上调用(如已运行完成或尚未开始的协程,行为未定义)
③ done()
- 作用:查询协程是否已执行完成(即是否已经到达函数体末尾或执行了co_return)
- 返回值:
true: 协程已执行完成,false: 协程仍在运行或处于挂起状态。
④ operator bool()
- 作用:查询是否是一个有效的协程句柄。
- 返回值:
true: 指向一个有效的协程,false: 协程句柄内存指向一个nullptr指针。
⑤ destroy()
- 作用:显式销毁协程帧,释放其内存。
- 何时调用:通常在协程返回类型的析构中调用,只有当协程在
final_suspend()返回std::suspend_always挂起时,你才需要(且必须)手动调用.destroy()。如果final_suspend()返回std::suspend_never,协程会自动销毁自己,此时再调用.destroy()就是未定义行为。 - 注意:忘记调用
.destroy()会导致内存泄漏。
⑥ operator coroutine_handle<>()
- 作用:该转换函数将
std::coroutine_handle<Promise>值转换为包含相同底层 address 的std::coroutine_handle<>值。 - 何时调用:
void await_suspend (std::coroutine_handle<> h)中作为形参接收各协程的句柄。
3.3 Awaitable 对象
在C++20协程中,任何可以出现在 co_await 表达式右边的对象,都是一个Awaitable对象。它的核心作用是:定义当协程执行到co_await expression 的行为,具体来说它控制三个方面的问题:
- 是否需要暂停当前协程?
- 在暂停之后、恢复之前,需要安排什么异步操作?
- 在恢复之后,如何获取异步操作的结果?
一个类型要成为Awaitable,它必须实现三个特定的成员函数 await_ready/await_suspend/await_resume ,或者通过 operator co_await 重载返回一个实现了这些函数的对象
编译器会将 co_await expression 转换为一系列对这些函数的调用,让我们来分解这个过程,如下:
{
// 1. 获取 awaitable 对象
auto&& awaitable = <expression>;
// 2. 检查是否 ready
if (!awaitable.await_ready()) {
// 3. 准备暂停
// 4. 调⽤ await_suspend,传⼊当前协程的句柄
// 5. 根据 await_suspend 的返回值决定⾏为(见下文)
// ...
<suspend coroutine> // 协程在此处暂停
// ... 异步操作在后台进行 ...
// ... 当异步操作完成时,通过某种方式恢复协程 ...
// 6. 协程在此时恢复执行
}
// 7. 获取结果
return awaitable.await_resume(); // 整个 co_await 表达式的结果
}
① bool await_ready() const
- 目的:性能优化。在尝试暂停之前,检查异步操作是否已经完成。
- 返回值:
true表示结果已就绪,无需暂停协程。编译器将跳过await_suspend和暂停步骤,直接调用await_resume并继续执行。false表示结果未就绪,需要暂停协程,继续执行await_suspend逻辑。 - 最佳实践通常实现为
noexcept。对于立即完成的操作(如缓存命中),返回 true 可以避免不必要的暂停开销
② type await_suspend( std::coroutine_handle<>) const
- 这是最强大也是最复杂的部分,它在协程即将暂停但尚未暂停时被调用。
- 目的:安排异步操作的回调,它的职责是获取当前协程的句柄
handle,并将其传递给某个异步操作,以便在操作完成后恢复协程。 - 参数:
std::coroutine_handle<>,代表当前正在执行的协程,可以保存、传递 - 返回值和行为:返回值类型至关重要,它决定了控制流的走向
- 返回值行为
void,默认行为,协程暂停,控制权返回到当前协程调用者。这是最常见的情况。 - 返回类型为
bool,true表示协程已挂起到后台(可能在另一个线程恢复);false表示不挂起,立即恢复,用于在最后关头发现需要等待的操作已完成的情况。 - 返回类型为
std::coroutine_handle<>,另一个协程句柄handle协程暂停,然后立即恢复handle所代表的协程,这实现了对称转移,是无栈协程链式调用的关键。
- 返回值行为
- 最佳实践:
await_suspend通常实现为noexcept,在这里你通常会调用某个异步API,并将handle作为其完成回调,绝对不要在此函数内阻塞。
struct promise_type {
std::coroutine_handle<> handle;
// ...
};
struct Awaiter {
bool await_ready() noexcept { return false; }
// 这里就是返回另一个协程句柄的地方
template<typename Promise>
std::coroutine_handle<> await_suspend(
std::coroutine_handle<Promise> current) noexcept {
// 获取当前协程的promise
auto& promise = current.promise();
// 返回另一个协程的句柄(如果存在)
if (promise.handle) {
return promise.handle; // 对称转移到另一个协程
}
else {
// 没有⽗协程,返回空句柄(回到主调线程)
return std::noop_coroutine();
}
}
void await_resume() noexcept {}
};
③ type await_resume()const
- 目的:获取结果或处理错误。在 协程恢复后 或
await_ready返回true时,此函数被调用以产生co_await表达式的结果。 - 返回值:可以是void或任何其他类型,这个返回值就是整个co_await 表达式的结果。
- 如果异步操作产生一个值,
await_resume()返回这个值。 - 如果异步操作可能失败,
await_resume()可以检查错误码并选择返回一个值或抛出一个异常。 - 最佳实践:可以抛出异常,不抛异常时标记为noexcept。
- 如果异步操作产生一个值,
C++20在标准库中提供了两个最简单的Awaitable类型
std::suspend_alwaysawait_ready():返回 false,总是暂停。await_suspend(...):无操作,直接返回void。await_resume():返回 void。- 用途:用于
initial_suspend()和final_suspend(),表示请在此处暂停
std::suspend_neverawait_ready():返回 true,从不暂停。await_suspend(...):永远不会被调用。await_resume():返回 void.- 用途:用于
initial_suspend()表示请不要暂停,立即开始执行协程体。
operator co_await()
- 定义:operatorco_await 是一个用户定义的转换函数,它允许将自定义类型转换为可以被co_await 使用的Awaitable类型。
auto operator co_await() const & noexcept;
auto operator co_await() && noexcept;
auto operator co_await() & noexcept;
#include <coroutine>
class AdvancedTask {
int value_;
public:
AdvancedTask(int value) : value_(value) {}
// 对于 const 左值 的 operator co_await
auto operator co_await() const & {
struct ConstLvalueAwaiter {
const AdvancedTask& task_;
bool await_ready() const { return false; }
void await_suspend(std::coroutine_handle<>) const {}
int await_resume() const {
return task_.value_ * 2; // 某种转换
}
};
return ConstLvalueAwaiter{*this};
}
// 对于 右值 的 operator co_await
auto operator co_await() && {
struct RvalueAwaiter {
AdvancedTask&& task_;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<>) {}
int await_resume() {
int result = task_.value_;
task_.value_ = -1; // 可以修改,因为是右值
return result;
}
};
return RvalueAwaiter{std::move(*this)};
}
// 对于 非const左值 的 operator co_await
auto operator co_await() & {
struct LvalueAwaiter {
AdvancedTask& task_;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<>) {}
int await_resume() {
task_.value_ += 10; // 可以修改
return task_.value_;
}
};
return LvalueAwaiter{*this};
}
};
// 调用
co_await AdvancedTask(10);
4. 协程样例
4.1 生成器
以下样例主要实现⼀个 co_yield 的协程,主要通过 Promise 对象和协程句柄控制协程,几乎是最简单的协程程序。
#include <coroutine>
#include <exception>
#include <iostream>
// 1. 协程的返回类型
struct Generator
{
// 2. 核心:promise_type
struct promise_type{
int current_value; // 用于存储产出的值
// 2.1 创建返回值对象
Generator get_return_object(){
return Generator{ std::coroutine_handle<promise_type>::from_promise(*this) };
}
// 2.2 初始挂起:选择挂起,让协程惰性执行
std::suspend_always initial_suspend() { return {}; }
// 2.3 最终挂起:选择挂起,需要手动销毁
std::suspend_always final_suspend() noexcept { return {}; }
// 2.4 处理 co_yield
std::suspend_always yield_value(int value)
{
current_value = value;
return {}; // 产出后总是挂起
}
// 2.5 处理 co_return; (无值返回)
void return_void() {}
// 2.6 处理异常
void unhandled_exception() { std::terminate(); }
};
// 3. Generator 类本身的成员
// 协程句柄,用于从外部控制协程
std::coroutine_handle<promise_type> handle;
// 构造函数和析构函数
explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
~Generator(){
if (handle)handle.destroy(); // 负责销毁协程帧
}
// 4. 提供给外部的API
// 获取当前值
int value() const{
return handle.promise().current_value;
}
// 恢复执行直到下一个co_yield或结束
bool next(){
if (!handle.done()) handle.resume();
return !handle.done();
}
};
// 使用这个生成器的协程函数
Generator range(int start, int end)
{
for (int i = start; i < end; ++i){
co_yield i; // 这会调用 promise.yield_value(i)
}
// 协程结束,调用 promise.return_void()
}
int main()
{
auto gen = range(1, 5); // 创建协程,initial_suspend挂起,此时协程未执行
while (gen.next()){
// 恢复协程,协程执行到co_yield处挂起
std::cout << gen.value() << " "; // 从promise中获取产出的值
}
// 输出: 1 2 3 4
}
执行流程图解
main() 调用 range(1,5)
↓
协程创建,执行 initial_suspend() → 暂停(惰性启动)
↓
gen.next() → handle.resume()
↓
协程执行: i=1 → co_yield 1 → yield_value(1) → 暂停
↓
gen.value() 拿到 1 → 输出
↓
gen.next() → resume() → i=2 → co_yield 2 → 暂停
↓
... 循环直到 i=4
↓
i=5 不满足 i<end → 协程结束 → final_suspend() → 暂停
↓
gen.next() 发现 handle.done() → 返回 false → 循环结束
↓
gen 析构 → handle.destroy() → 释放协程帧
详细分析:
① 协程创建阶段(触发: auto gen = range(1,5) )
- 核心逻辑:调用 range 协程函数时,编译器自动创建协程帧(存储局部变量 i 、参数start=1/end=5 、 promise_type 实例),并通过 promise 衔接 Generator 。
- 代码映射:
- 编译器自动构造 promise_type 实例(初始化 current_value );
- 调用
promise.get_return_object():创建 Generator 对象,绑定coroutine_handle<promise_type>(通过from_promise(*this)关联 promise ); - Generator 构造函数执行:保存 handle (后续控制协程的 “手柄”)。
② 初始挂起状态(触发: initial_suspend() )
- 核心逻辑:协程创建后不立即执行,而是先挂起( suspend_always 表⽰“总是挂起”),实现惰性执行(按需启动)。
- 代码映射:
promise.initial_suspend()返回std::suspend_always{},协程暂停在 “执行前” 状态;- 此时 main 中拿到的 gen 已就绪,但 range 的 for 循环尚未开始执行。
③ 恢复执行阶段(触发: gen.next() )
- 核心逻辑:外部通过 Generator 的 next() 接口,用 handle 唤醒协程,执行到下一个 co_yield 或协程结束。
- 代码映射:
gen.next()判断!handle.done()(当前协程未结束),调用handle.resume();- 协程从挂起点恢复,执行 range 的逻辑:
- 第一次恢复:进入 for 循环, i=start=1 ,执行 co_yield 1 ;
- 后续恢复:从 co_yield 的挂起点继续, i++ (如 i=2 ),再次执行 co_yield 2 ,直到 i>=end ( i=4 后 i++=5 ,退出循环)。
④ 产出值挂起状态(触发: co_yield )
- 核心逻辑:co_yield是协程“产出值”的关键字,本质是调用promise.yield_value(),并挂起协程,让外部获取产出值。
- 代码映射:
- co_yield value 调用
promise.yield_value(value), 将 value 存入promise.current_ value; yield_value()返回std::suspend_always{},协程再次挂起;- 此时
gen.next()返回!handle.done()(true),外部通过gen.value()读取promise.current_value(如第一次读取1)
- co_yield value 调用
⑤ 协程结束状态(触发: for 循环退出)
- 核心逻辑:range的for循环执行完毕( i = 5 > = e n d = 5 i=5>=end=5 i=5>=end=5),协程无更多代码可执行,进入结束流程。
- 代码映射:
- 协程执行到函数末尾,编译器自动调用
promise.return_void()(处理co_return;无值返回的场景) - 此时协程标记为 “已结束”(
handle.done()变为true)
- 协程执行到函数末尾,编译器自动调用
⑥ 最终挂起状态(触发: final_suspend() )
- 核心逻辑:协程结束后,通过
final_suspend()挂起,等待外部手动销毁(避免协程帧提前释放) - 代码映射:
promise.final_suspend()返回std::suspend_always{},协程保持挂起状态;- 此时
gen.next()再次调用时,handle.done()为true,next()返回false, main 的 while循环退出。
⑦ 协程销毁阶段(触发: gen 析构)
- 核心逻辑:main函数结束,Generator对象gen出作用域,析构函数释放协程帧内存。
- 代码映射:
Generator::~Generator()判断 handle 有效,调用handle.destroy();- 销毁协程帧(释放i、start、end、promise_type实例的内存),协程生命周期完全结束。
换一种方式,跟上面代码功能类似,实现支持迭代器的协程返回对象。
#include <coroutine>
#include <iostream>
template <typename T>
struct Generator
{
struct promise_type{
T current_value;
Generator get_return_object(){
return Generator{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() { std::terminate(); }
std::suspend_always yield_value(T value){
current_value = value;
return {};
}
void return_void() {}
};
struct iterator{
std::coroutine_handle<promise_type> handle;
iterator(std::coroutine_handle<promise_type> h = nullptr) : handle(h){}
iterator& operator++(){
if (handle && !handle.done()) handle.resume();
return *this;
}
T operator*() const{
return handle.promise().current_value;
}
bool operator!=(const iterator& other) const{
return !handle.done();
}
};
explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
~Generator(){
if (handle)handle.destroy();
}
iterator begin(){
if (handle)handle.resume();
return iterator{ handle };
}
iterator end() { return iterator{}; }
private:
std::coroutine_handle<promise_type> handle;
};
// =============== 使用示例 ==================
Generator<int> range(int start, int end)
{
for (int i = start; i < end; ++i)
{
co_yield i; // 产生值并挂起
}
}
Generator<int> fibonacci(int limit){
int a = 0, b = 1;
while (a <= limit){
co_yield a;
int next = a + b;
a = b;
b = next;
}
}
int main()
{
std::cout << "Range 1-5: ";
for (auto i : range(1, 6)){
std::cout << i << " ";
}
std::cout << std::endl;
/* 等价写法
auto gen = range(1, 6);
auto it = gen.begin();
while (it != gen.end()) {
std::cout << *it << std::endl;
++it;
}*/
std::cout << "Fibonacci below 50: ";
for (auto num : fibonacci(50)){
std::cout << num << " ";
}
std::cout << std::endl;
}
4.2 异步线程恢复
这个样例展示了 co_await 的使用方法,其次就是展示了,协程可以在另一个线程中恢复执行后续逻辑
#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>
// 增强的可等待对象,添加更多日志
struct AsyncOperation
{
int value;
bool ready = false;
const char* name;
AsyncOperation(int v, bool r, const char* n = "")
: value(v), ready(r), name(n)
{
std::cout << "[" << name << "] AsyncOperation 构造: value=" << value << ", ready=" << ready << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
}
bool await_ready() const noexcept
{
std::cout << "[" << name << "] await_ready() 调用, 返回: " << ready << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
return ready;
}
void await_suspend(std::coroutine_handle<> handle) noexcept
{
std::cout << "[" << name << "] await_suspend() 调用, 协程句柄: " << handle.address() << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
// 启动异步操作
std::thread([this, handle, name = this->name]() {
std::cout << "[" << name << "] 异步线程开始 (线程: " << std::this_thread::get_id() << ")" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
this->ready = true;
std::cout << "[" << name << "] 异步操作完成, 准备恢复协程 (线程: " << std::this_thread::get_id() << ")" << std::endl;
// 恢复协程执行
handle.resume();
std::cout << "[" << name << "] 异步线程结束 (线程: " << std::this_thread::get_id() << ")" << std::endl;
}).detach();
std::cout << "[" << name << "] await_suspend() 返回, 协程即将挂起 (线程: " << std::this_thread::get_id() << ")" << std::endl;
}
int await_resume() noexcept
{
std::cout << "[" << name << "] await_resume() 调用, 返回结果: " << value << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
return value;
}
};
struct Task
{
struct promise_type
{
int current_value;
Task get_return_object()
{
std::cout << "promise_type::get_return_object() 调用" << std::endl;
return Task{
std::coroutine_handle<promise_type>::from_promise(*this) };
}
std::suspend_never initial_suspend() noexcept
{
std::cout << "promise_type::initial_suspend() 调用 - 立即开始执行"
<< std::endl;
return {};
}
std::suspend_always final_suspend() noexcept
{
std::cout << "promise_type::final_suspend() 调用 - 协程结束" << std::endl;
return {};
}
void unhandled_exception()
{
std::cout << "promise_type::unhandled_exception() 调用" << std::endl;
}
void return_void()
{
std::cout << "promise_type::return_void() 调用 - 协程正常返回" << std::endl;
}
};
std::coroutine_handle<promise_type> handle;
Task(std::coroutine_handle<promise_type> h) : handle(h)
{
std::cout << "Task 对象构造, 句柄: " << handle.address() << std::endl;
}
~Task()
{
if (handle)
{
std::cout << "Task 析构, 销毁协程句柄" << std::endl;
handle.destroy();
}
}
};
Task detailed_async_example()
{
std::cout << "=== 协程函数开始执行 ===" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
std::cout << "\n--- 第一次 co_await ---" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
int result1 = co_await AsyncOperation{ 100, false, "操作1" };
std::cout << "第一次 co_await 后继续执行, 结果: " << result1 << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
std::cout << "\n--- 第二次 co_await ---" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
int result2 = co_await AsyncOperation{ 200, false, "操作2" };
std::cout << "第二次 co_await 后继续执行, 结果: " << result2 << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
std::cout << "\n--- 第三次 co_await (就绪状态) ---" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
int result3 = co_await AsyncOperation{ 300, true, "操作3" };
std::cout << "第三次 co_await 后继续执行, 结果: " << result3 << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
std::cout << "\n=== 协程函数执行完成 ===" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
}
int main()
{
std::cout << "主函数开始 (线程: " << std::this_thread::get_id() << ")" << std::endl;
{
std::cout << "\n********** 创建协程任务 **********" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
auto task = detailed_async_example();
std::cout << "协程任务已创建,控制权返回主函数" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
// 给异步操作时间完成
std::cout << "\n主线程等待 3 秒..." << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3));
std::cout << "主线程等待结束" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
} // task 析构
std::cout << "\n主函数结束" << " (线程: " << std::this_thread::get_id() << ")" << std::endl;
return 0;
}
这样子除了第一次挂起是和主线程相关的,后续的协程恢复和执行都是在其他线程上执行的,不会影响到主线程
输出如下:
主函数开始 (线程: 45368)
********** 创建协程任务 ********** (线程: 45368)
promise_type::get_return_object() 调用
Task 对象构造, 句柄: 000001D1E640A6B0
promise_type::initial_suspend() 调用 - 立即开始执行
=== 协程函数开始执行 === (线程: 45368)
--- 第一次 co_await --- (线程: 45368)
[操作1] AsyncOperation 构造: value=100, ready=0 (线程: 45368)
[操作1] await_ready() 调用, 返回: 0 (线程: 45368)
[操作1] await_suspend() 调用, 协程句柄: 000001D1E640A6B0 (线程: 45368)
[操作1] await_suspend() 返回, 协程即将挂起 (线程: 45368)
协程任务已创建,控制权返回主函数 (线程: 45368)
主线程等待 3 秒... (线程: 45368)
[操作1] 异步线程开始 (线程: 34676)
[操作1] 异步操作完成, 准备恢复协程 (线程: 34676)
[操作1] await_resume() 调用, 返回结果: 100 (线程: 34676)
第一次 co_await 后继续执行, 结果: 100 (线程: 34676)
--- 第二次 co_await --- (线程: 34676)
[操作2] AsyncOperation 构造: value=200, ready=0 (线程: 34676)
[操作2] await_ready() 调用, 返回: 0 (线程: 34676)
[操作2] await_suspend() 调用, 协程句柄: 000001D1E640A6B0 (线程: 34676)
[操作2] await_suspend() 返回, 协程即将挂起 (线程: [操作234676)] 异步线程开始 (线程: 38044)
[操作1] 异步线程结束 (线程: 34676)
[操作2] 异步操作完成, 准备恢复协程 (线程: 38044)
[操作2] await_resume() 调用, 返回结果: 200 (线程: 38044)
第二次 co_await 后继续执行, 结果: 200 (线程: 38044)
--- 第三次 co_await (就绪状态) --- (线程: 38044)
[操作3] AsyncOperation 构造: value=300, ready=1 (线程: 38044)
[操作3] await_ready() 调用, 返回: 1 (线程: 38044)
[操作3] await_resume() 调用, 返回结果: 300 (线程: 38044)
第三次 co_await 后继续执行, 结果: 300 (线程: 38044)
=== 协程函数执行完成 === (线程: 38044)
promise_type::return_void() 调用 - 协程正常返回
promise_type::final_suspend() 调用 - 协程结束
[操作2] 异步线程结束 (线程: 38044)
主线程等待结束 (线程: 45368)
Task 析构, 销毁协程句柄
主函数结束 (线程: 45368)
协程 co_await 创建了一个恢复线程然后回到主线程,主线程就开始了自己的独立运行了,此时恢复线程恢复了协程,协程继续执行到下一个 co_await 又创建了一个恢复线程2,然后挂起回到恢复线程1,恢复线程1销毁,然后恢复线程2恢复协程执行
4.3 echo服务器
以下代码实现了一个简单epoll+协程的回声服务器,帮助我们理解协程一些使用场景,需要时gcc10及以后版本上运行,因为这个版本开始才支持协程。
// 编译命令: g++ -std=c++20 -pthread test.cpp -o server
// 运行: ./server
// 测试: telnet localhost 9000
#include <iostream>
#include <coroutine>
#include <thread>
#include <vector>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <memory>
#include <array>
// 将文件描述符设置为非阻塞模式
static void set_nonblocking(int fd)
{
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1){
perror("fcntl F_GETFL");
return;
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1){
perror("fcntl F_SETFL O_NONBLOCK");
}
}
// I/O 等待器:用于协程等待 epoll 事件
struct IoAwaiter
{
int fd; // 要等待的文件描述符
int events; // 要等待的事件类型:EPOLLIN(可读) 或 EPOLLOUT(可写)
epoll_event ev{}; // epoll 事件结构
// 静态成员,保存 epoll 实例的文件描述符(全局共享)
static inline int epfd = -1;
// 协程等待开始前调用,返回 false 表示需要挂起等待
bool await_ready() const noexcept{
return false; // 总是挂起,交给 epoll 通知
}
// 挂起协程,并将事件注册到 epoll
void await_suspend(std::coroutine_handle<> h) noexcept{
// 将协程句柄存到 epoll_event 的 data.ptr 中,用于事件触发时恢复
ev.data.ptr = h.address();
// 设置事件类型,并使用边缘触发(EPOLLET)
ev.events = events | EPOLLET;
// 将 fd 和事件注册到 epoll
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
}
// 协程恢复后调用,从 epoll 中移除该事件
void await_resume() noexcept{
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev);
}
};
// 等待 accept 事件的协程等待器(包装函数)
IoAwaiter co_accept(int fd) { return IoAwaiter{ fd, EPOLLIN }; }
// 等待读事件的协程等待器(包装函数)
IoAwaiter co_read(int fd) { return IoAwaiter{ fd, EPOLLIN }; }
// 等待写事件的协程等待器(包装函数)
IoAwaiter co_write(int fd) { return IoAwaiter{ fd, EPOLLOUT }; }
// 协程任务类型(最小实现)
struct Task
{
struct promise_type{
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() noexcept { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
// 每个客户端连接的处理协程(Echo 逻辑)
Task echo_session(int sock)
{
try{
std::array<char, 4096> buf; // 缓冲区
for (;;){
// 等待 socket 可读(协程挂起,控制权返回事件循环)
co_await co_read(sock);
// 协程恢复后执行:读取数据
ssize_t n = read(sock, buf.data(), buf.size());
if (n <= 0){
// 连接关闭或出错
if (n < 0) perror("read");
break;
}
// 等待 socket 可写(协程再次挂起)
co_await co_write(sock);
// 协程恢复后执行:回写数据(Echo)
write(sock, buf.data(), n);
}
}catch (...){
std::cerr << "Session error\n";
}
close(sock); // 关闭连接,清理资源
}
// 监听协程:接受新连接并创建会话协程
Task listener(int port)
{
// 创建监听 socket(SOCK_NONBLOCK 创建时即设为非阻塞)
int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
if (s < 0){
perror("socket");
co_return;
}
// 设置 socket 选项:允许地址重用
int opt = 1;
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 绑定地址和端口
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡
addr.sin_port = htons(port); // 端口号
if (bind(s, (sockaddr*)&addr, sizeof(addr)) < 0){
perror("bind");
co_return;
}
// 开始监听
if (listen(s, SOMAXCONN) < 0){
perror("listen");
co_return;
}
std::cout << "Listening on port " << port << std::endl;
// 循环接受新连接
for (;;)
{
// 等待监听 socket 可读(有新连接),协程挂起
co_await co_accept(s);
// 协程恢复后执行:接受连接
sockaddr_in peer{};
socklen_t len = sizeof(peer);
int client = accept4(s, (sockaddr*)&peer, &len, SOCK_NONBLOCK);
if (client < 0){
if (errno != EAGAIN && errno != EWOULDBLOCK){
perror("accept");
}
continue;
}
std::cout << "New connection from " << inet_ntoa(peer.sin_addr) << std::endl;
// 为新连接创建会话协程(注意:这里只是创建,协程会立即执行到第一个 co_await)
echo_session(client);
}
}
int main()
{
// 创建 epoll 实例
IoAwaiter::epfd = epoll_create1(0);
if (IoAwaiter::epfd < 0){
perror("epoll_create1");
return 1;
}
// 启动监听协程(initial_suspend=suspend_never,所以立即执行到第一个 co_await)
listener(9000);
// 事件循环:单线程调度所有协程
std::vector<epoll_event> events(64); // 事件数组
for (;;){
// 等待事件发生(阻塞,直到有 IO 就绪)
int nfds = epoll_wait(IoAwaiter::epfd, events.data(), events.size(), -1);
if (nfds < 0){
perror("epoll_wait");
break;
}
// 处理每个就绪事件
for (int i = 0; i < nfds; ++i){
// 从事件中取出协程句柄并恢复执行
auto h = std::coroutine_handle<>::from_address(events[i].data.ptr);
h.resume();
}
}
close(IoAwaiter::epfd);
return 0;
}
整体架构图
┌─────────────────────────────────────┐
│ 🎯 单线程事件循环 (main) │
│ • epoll_wait() 等待 IO 事件 │
│ • 事件就绪 → 取出协程句柄 → resume()│
└─────────────────────────────────────┘
↑
│ epoll_ctl(ADD/DEL)
↓
┌─────────────────────────────────────┐
│ 🌀 协程 + IoAwaiter (桥梁) │
│ • co_await co_read(fd) │
│ • await_suspend: 注册 fd 到 epoll │
│ • await_resume: 从 epoll 移除 fd │
└─────────────────────────────────────┘
↑
│ co_await 触发
↓
┌─────────────────────────────────────┐
│ 📡 业务协程 (listener / echo_session)│
│ • 同步写法: co_await → read → write │
│ • 异步执行: IO 阻塞时自动挂起 │
└─────────────────────────────────────┘
🚀 执行流程详解(按时间顺序)
阶段 1:初始化(main 函数)
// 1. 创建 epoll 实例
IoAwaiter::epfd = epoll_create1(0); // epfd = 3
// 2. 启动监听协程
listener(9000);
listener 协程执行流程:
listener(9000) 被调用
↓
initial_suspend() → suspend_never → 立即执行(不暂停)
↓
创建监听 socket (s=4),bind(9000),listen()
↓
输出: "Listening on port 9000"
↓
进入 for(;;) 循环
↓
co_await co_accept(s) // 第一个暂停点!
↓
【IoAwaiter 三阶段】:
1. await_ready() → false → 需要挂起
2. await_suspend(h):
• ev.data.ptr = h.address() // 保存协程句柄
• ev.events = EPOLLIN|EPOLLET
• epoll_ctl(epfd, ADD, s, &ev) // 注册监听 socket 到 epoll
3. 协程暂停,控制权返回 main
↓
main 继续执行 → 进入事件循环
阶段 2:事件循环(main 函数)
for (;;)
{
// 阻塞等待 epoll 事件(无事件时主线程休眠,不消耗 CPU)
int nfds = epoll_wait(IoAwaiter::epfd, events.data(), events.size(), -1);
// 事件就绪,处理每个事件
for (int i = 0; i < nfds; ++i)
{
// 从 epoll_event.data.ptr 取出协程句柄
auto h = std::coroutine_handle<>::from_address(events[i].data.ptr);
h.resume(); // 恢复对应协程
}
}
阶段 3:新连接到达(客户端 telnet localhost 9000)
事件触发流程:
客户端连接 → 内核通知 epoll → epoll_wait 返回 nfds=1
↓
events[0].data.ptr 指向 listener 协程的句柄
↓
h.resume() 恢复 listener 协程
↓
【IoAwaiter::await_resume()】:
epoll_ctl(epfd, DEL, s, &ev) // 移除监听 socket 的注册
↓
listener 协程从 co_await co_accept(s) 之后继续执行
↓
accept4(s, ...) 接受新连接 → client=5
↓
输出: "New connection from 127.0.0.1"
↓
echo_session(client) // 创建新协程处理该连接
echo_session 协程执行流程:
echo_session(5) 被调用
↓
initial_suspend() → suspend_never → 立即执行
↓
进入 for(;;) 循环
↓
co_await co_read(5) // 等待客户端数据
↓
【IoAwaiter 三阶段】:
1. await_ready() → false
2. await_suspend(h):
• ev.data.ptr = h.address()
• epoll_ctl(epfd, ADD, 5, &ev) // 注册 client socket
3. 协程暂停,控制权返回 main
↓
main 继续 epoll_wait 等待其他事件
阶段 4:客户端发送数据
客户端发送 "hello" → 内核通知 epoll → epoll_wait 返回
↓
events[0].data.ptr 指向 echo_session 协程句柄
↓
h.resume() 恢复 echo_session 协程
↓
【IoAwaiter::await_resume()】:
epoll_ctl(epfd, DEL, 5, &ev)
↓
echo_session 从 co_await co_read(5) 之后继续
↓
read(5, buf, 4096) → n=5, buf="hello"
↓
co_await co_write(5) // 等待 socket 可写
↓
【IoAwaiter 三阶段】: 注册 EPOLLOUT 事件 → 协程暂停
↓
[通常 socket 立即可写,epoll 立即通知]
↓
h.resume() 恢复 echo_session
↓
await_resume() 移除 epoll 注册
↓
write(5, "hello", 5) // Echo 回客户端
↓
循环回到 co_await co_read(5) → 继续等待...
阶段 5:客户端断开连接
客户端关闭连接 → epoll 通知 EPOLLIN + EOF
↓
echo_session 恢复 → read() 返回 0
↓
if (n <= 0) break; // 退出循环
↓
close(sock) // 关闭 socket
↓
协程执行到末尾 → return_void() → final_suspend(suspend_never)
↓
协程帧自动销毁(Task 是空结构体,无资源需手动清理)
运行结果如下:

以下代码是使用 Boost.Asio 从1.74.0版本及以后版本(这个版本开始支持C++20协程),实现的一个简单的回声服务,展示了如果库支持以后,协程和网络编程配合实现,不需要像上面这么麻烦,由我们去控制协程三大组件的细节。
// sudo apt update
// sudo apt install libboost-all-dev
// g++ -std=c++20 echo_server.cpp -o echo_server -lboost_system
#include <iostream>
#include <array>
#include <boost/asio.hpp>
using boost::asio::ip::tcp;
// 会话协程:处理⼀个客户端连接
boost::asio::awaitable<void> echo_session(tcp::socket socket)
{
try
{
std::array<char, 4096> buf;
for (;;)
{
// 异步读数据
std::size_t n = co_await socket.async_read_some(
boost::asio::buffer(buf), boost::asio::use_awaitable);
// 异步写回数据
co_await boost::asio::async_write(socket, boost::asio::buffer(buf, n), boost::asio::use_awaitable);
}
}
catch (std::exception &e)
{
std::cerr << "Session error: " << e.what() << std::endl;
}
}
// 监听协程:接受新连接
boost::asio::awaitable<void> listener(unsigned short port)
{
tcp::acceptor acc(co_await boost::asio::this_coro::executor,
tcp::endpoint(tcp::v4(), port));
std::cout << "Listening on port " << port << std::endl;
for (;;)
{
tcp::socket socket = co_await acc.async_accept(boost::asio::use_awaitable);
std::cout << "New connection from " << socket.remote_endpoint() << std::endl;
// 启动新会话(不等待它完成)
boost::asio::co_spawn(acc.get_executor(),
echo_session(std::move(socket)),
boost::asio::detached);
}
}
int main()
{
try{
boost::asio::io_context io;
// 启动监听协程
boost::asio::co_spawn(io, listener(9000), boost::asio::detached);
// 运行事件循环
io.run();
}catch (std::exception &e){
std::cerr << "Server error: " << e.what() << std::endl;
}
}
5. 小结
下面这个流程图完整展示了 C++20 协程的生命周期:
📌 主要阶段
- 协程初始化阶段 - 创建协程帧、分配内存、构造 promise 对象
- 初始挂起决策 - 通过
initial_suspend决定是否立即执行 - 协程体执行循环 - 执行用户代码,遇到不同操作分支处理
- co_await 详细流程 - 异步等待的核心机制
- co_yield 详细流程 - 生成器模式的实现
- co_return 详细流程 - 协程正常结束
- 异常处理 - 未捕获异常的处理
- 销毁阶段 - 协程帧的清理和内存释放
- 恢复机制 - 外部如何恢复挂起的协程
🔑 关键决策点
await_ready()- 决定是否立即执行还是挂起initial_suspend()/final_suspend()- 控制协程的启动和结束行为coroutine_handle.resume()- 外部恢复协程的入口
【★,°:.☆( ̄▽ ̄)/$:.°★ 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【C++20】的内容,请持续关注我 !!

更多推荐

所有评论(0)