C++20 引入了协程(coroutine)这一强大的语言特性,为编写异步代码提供了全新的语法与机制。与传统的回调、线程或事件循环相比,协程让代码结构更加直观、可维护。本文将从协程的基础概念、实现机制、典型使用场景以及注意事项四个角度,系统剖析 C++20 协程的实用价值。
1. 协程的基本概念
协程是一种比线程更轻量级的“子程序”,它可以在执行过程中挂起(yield)并在稍后恢复。C++20 通过 co_await、co_yield 与 co_return 三个关键字以及协程生成器和任务类型实现了协程的语义。
-
生成器(Generator)
通过co_yield产生一系列值,调用者使用for (auto&& x : generator)进行迭代。 -
任务(Task)
通过co_return返回一个单值或 void,使用co_await进行等待。 -
调度器(Scheduler)
决定协程何时恢复执行。标准库默认提供了最小化的调度机制,但在真实项目中往往需要自定义调度器,例如基于线程池、事件循环或回调队列。
2. 协程的实现机制
C++ 的协程编译器在内部会生成一个 promise 对象,该对象存放协程的状态(返回值、异常、挂起点等)。协程入口函数被转换为一个 state machine,每次遇到 co_await、co_yield 或 co_return 都会产生一个挂起点。
2.1 Promise 类型
template<typename T>
struct Promise {
T value_;
std::exception_ptr ex_;
auto get_return_object() {
return Task <T>{/*...*/};
}
auto initial_suspend() { return std::suspend_always{}; }
auto final_suspend() noexcept { return std::suspend_always{}; }
void unhandled_exception() { ex_ = std::current_exception(); }
template<typename U>
void return_value(U&& v) { value_ = std::forward <U>(v); }
};
initial_suspend与final_suspend控制协程的起始与结束挂起行为。unhandled_exception负责捕获未处理的异常并保存在 promise 中。return_value存储最终返回值。
2.2 Awaiter
co_await 操作需要一个 Awaiter 对象,该对象实现以下接口:
struct Awaiter {
bool await_ready();
void await_suspend(std::coroutine_handle<> h);
auto await_resume();
};
await_ready判断是否立即完成。await_suspend在需要挂起时调用,将协程句柄保存到调度器。await_resume在协程恢复时返回结果。
3. 典型使用场景
| 场景 | 传统做法 | 协程做法 | 优点 |
|---|---|---|---|
| 异步 I/O | 线程 + I/O 事件循环 | async_io_task + co_await |
代码更像同步,避免回调地狱 |
| 并行计算 | 线程池 + 共享内存 | 并行任务 + co_await |
更细粒度的协作 |
| 数据流 | 生产者消费者 + 队列 | 生成器 + co_yield |
简化状态机 |
| GUI 事件 | 事件回调 | 协程 + 调度器 | 逻辑更清晰,易于维护 |
示例:文件读取异步任务
#include <coroutine>
#include <iostream>
#include <fstream>
#include <string>
struct AsyncRead {
struct promise_type {
std::string result;
AsyncRead get_return_object() { return AsyncRead{std::coroutine_handle <promise_type>::from_promise(*this)}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
void return_void() {}
};
std::coroutine_handle <promise_type> h;
};
AsyncRead read_file_async(const std::string& path) {
std::ifstream in(path);
std::string line;
while (std::getline(in, line)) {
co_yield line; // 这里可以放进生成器
}
}
实际使用时,你会把 co_yield 替换为 co_await async_read_line(),其中 async_read_line 是返回 Awaitable 的异步 I/O 操作。
4. 需要注意的问题
-
异常安全
若协程内部抛出异常,promise 的unhandled_exception会被调用。若想在外层捕获,需要将异常存储在 promise 并在await_resume里抛出。 -
资源管理
协程对象自身是轻量级的,但在协程内部使用非栈对象时需使用 RAII 进行显式释放,尤其是与 I/O 句柄、网络连接等外部资源交互时。 -
调度器设计
默认的std::suspend_always只会让协程挂起但不调度。若需要真正并发执行,需要自定义调度器。常见做法:使用std::async、线程池或基于asio的事件循环。 -
性能成本
协程的 state machine 产生的栈开销与函数调用相比略高。若对性能极致要求,需仔细评估协程与传统回调或线程的开销差异。
5. 结语
C++20 的协程为异步编程提供了现代化、类型安全且与同步代码高度兼容的实现方式。通过合理的 promise、awaitable 与调度器设计,你可以把复杂的事件驱动逻辑写成几乎线性的代码,极大提升可读性和可维护性。未来的 C++ 标准会继续完善协程相关的库支持(如 std::execution、std::ranges 与协程的结合),让异步编程更加成熟与普及。希望这篇文章能帮助你快速上手并在项目中有效运用 C++20 协程。