在C++20中,协程(coroutines)被正式纳入语言标准,提供了一种更高层次的异步编程模型。相比传统的回调或Future/Promise,协程让代码更像同步流程,更易读、维护。本文将从语法、实现原理、典型使用场景以及常见坑点四个角度,系统地剖析C++20协程的核心概念与实践。
1. 协程基本语法
1.1 co_await、co_yield、co_return
co_await:等待一个协程或异步操作完成,类似await。co_yield:生成一个值给消费者,类似生成器。co_return:结束协程,返回最终结果。
1.2 协程函数的返回类型
C++20将协程函数的返回类型分为三类:
- `std::future ` / `std::shared_future`(标准库实现)
- `std::generator `(C++23提供)
- 自定义 `awaitable ` 或 `generator`(常见于Boost.Asio等)
编译器会自动插入一个“promise”对象,协程函数的状态机由此生成。
1.3 示例代码
#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
Task simple() {
std::cout << "Before co_await\n";
co_await std::suspend_always{}; // 模拟异步等待
std::cout << "After co_await\n";
}
int main() {
simple();
std::cout << "Program end\n";
return 0;
}
该程序会先输出 “Before co_await”,随后暂停,最后在 main 结束前输出 “After co_await”。
2. 协程实现原理
2.1 状态机生成
编译器把协程函数拆分为若干“步”,每个 co_await/co_yield/co_return 处都会产生一条分支。所有分支通过一个 switch 或状态机表驱动,保证程序在暂停后能够恢复到正确的位置。
2.2 Promise 对象
Promise 对象负责:
- 保存协程局部变量(如
this、参数等) - 提供
get_return_object、initial_suspend、final_suspend、return_void/return_value、unhandled_exception等接口 - 存放协程的“悬挂”或“完成”状态
2.3 资源管理
协程生成器对象与其 promise_type 必须在同一生命周期内,否则会出现悬挂的 std::coroutine_handle。
co_return时,final_suspend的返回值决定协程是否自动销毁。- 若返回
std::suspend_never,协程会立即完成并销毁。
3. 常见协程使用场景
3.1 异步 I/O
在 std::async 或网络库中,协程可替代回调链,让异步 I/O 看似同步。
awaitable<std::string> fetch_from_server(const std::string& url) {
auto socket = co_await tcp_connect(url);
std::string resp = co_await socket.read_some();
co_return resp;
}
3.2 生成器
使用 co_yield 可实现懒加载序列,例如斐波那契数列、文件行迭代器等。
generator <int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
int tmp = a + b;
a = b;
b = tmp;
}
}
3.3 任务调度
在游戏引擎或 UI 框架中,协程可做帧间调度,让长时间任务分步执行。
4. 常见坑与最佳实践
| 序号 | 坑点 | 解决方案 |
|---|---|---|
| 1 | 协程对象被移动后访问 coroutine_handle |
确保协程对象在移动前后保持有效,避免裸指针 |
| 2 | co_await 的异步对象没有 await_ready/await_suspend |
自定义 awaitable 时实现完整协议 |
| 3 | 内存泄漏:promise 未被销毁 | final_suspend 必须返回 std::suspend_never 或手动销毁 handle |
| 4 | 性能开销 | 使用 std::suspend_always / std::suspend_never 选择性暂停,避免不必要的上下文切换 |
| 5 | 与旧版库混用 | 尽量使用统一的异步框架(如 Boost.Asio),避免不同协程实现混合 |
4.1 自定义 awaitable 示例
struct Timer {
std::chrono::milliseconds duration;
Timer(std::chrono::milliseconds d) : duration(d) {}
bool await_ready() const noexcept { return duration.count() == 0; }
void await_suspend(std::coroutine_handle<> h) {
std::thread([h, this](){
std::this_thread::sleep_for(duration);
h.resume();
}).detach();
}
void await_resume() const noexcept {}
};
调用方式:
co_await Timer( std::chrono::seconds(1) );
5. 小结
C++20 的协程为语言带来了更接近自然流程的异步编程模型。通过了解协程的语法、状态机实现以及 Promise 的角色,程序员可以在保持代码可读性的同时,高效地实现异步 I/O、生成器、任务调度等功能。与此同时,需要注意协程对象的生命周期、awaitable 的完整协议以及性能权衡,避免常见陷阱。随着未来标准的进一步完善(C++23 将推出 std::generator 等),协程将在更广泛的场景中发挥作用。