协程(coroutine)是 C++20 引入的一项强大特性,它让我们能够编写非阻塞、可暂停的函数,从而简化异步编程、生成器以及复杂的控制流。本文将从协程的基本概念、语法实现、常见用例以及性能考量等方面进行系统阐述,帮助读者快速掌握并在项目中实际应用。
一、协程的基本概念
- 可暂停函数
与传统函数一次性完成所有工作不同,协程可以在执行过程中“挂起”(co_await、co_yield、co_return),让调用方在需要时继续执行。
- 状态机
协程内部被编译器转换为一个隐式的状态机,每一次挂起点对应一个状态,协程的状态由 std::coroutine_handle 管理。
- 协程句柄
std::coroutine_handle 提供对协程实例的控制(resume、destroy 等),并可与自定义的 promise 类型配合使用。
二、协程的关键语法
| 关键字 |
作用 |
示例 |
co_await |
挂起协程,等待另一个协程或 awaitable 对象完成 |
int n = co_await async_add(5, 3); |
co_yield |
产生值并挂起协程,常用于生成器 |
co_yield i; |
co_return |
结束协程并返回值 |
co_return result; |
协程的主体与普通函数相似,只是返回类型不是普通的 T,而是 `std::future
`、`std::generator` 或自定义类型。返回类型的 `promise_type` 定义了协程的行为。
—
## 三、协程的实现细节
1. **Promise 类型**
每个协程都有一个 `promise_type`,负责:
– 产生 `co_await`、`co_yield` 的行为
– 管理协程的生命周期
– 处理异常
例如:
“`cpp
struct Awaitable {
struct promise_type {
Awaitable get_return_object() { return {}; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
void return_void() {}
};
};
“`
2. **挂起与恢复**
`co_await` 在遇到 `std::suspend_always` 或 `std::suspend_never` 时决定是否挂起。
`co_yield` 通过返回值给调用方,并挂起。
调用方使用 `coroutine_handle::resume()` 继续执行。
3. **异常传播**
如果协程内部抛出异常,`promise_type::unhandled_exception` 会被调用。若返回类型是 `std::future`,异常会被包装在 `future` 中。
—
## 四、常见协程用例
### 1. 异步 IO
利用 `co_await` 与异步 I/O 库(如 `boost::asio`、`libuv`)配合,让网络请求、文件读取等操作在单线程中并发执行,而无需手写事件循环。
“`cpp
std::future read_file(const std::string& path) {
std::ifstream file(path, std::ios::binary);
if (!file) co_return std::string();
std::string content((std::istreambuf_iterator
(file)),
std::istreambuf_iterator
());
co_return content;
}
“`
### 2. 生成器
使用 `co_yield` 实现惰性序列,例如斐波那契数列、无限范围等。
“`cpp
std::generator
fib() {
int a = 0, b = 1;
while (true) {
co_yield a;
int next = a + b;
a = b;
b = next;
}
}
“`
### 3. 状态机简化
将复杂的状态机实现为多段 `co_yield` 或 `co_await`,代码可读性大幅提升。
“`cpp
std::future
state_machine() {
// 状态 A
co_await async_task_A();
// 状态 B
co_await async_task_B();
// 状态 C
co_return;
}
“`
—
## 五、性能与资源管理
1. **栈占用**
协程的状态机不再使用传统栈帧,而是由编译器分配堆区或自定义缓冲区。对于高频调用的协程,应考虑使用 `std::coroutine_handle::promise_type::return_handle()` 提前释放资源。
2. **延迟生成**
生成器的 `co_yield` 会在每次 `resume` 时重新构造 `value_type`,若类型较大,建议使用引用或指针。
3. **异常开销**
异常传播需要保存异常对象,频繁抛异常会显著影响性能。协程内部应尽量使用错误码或 `std::optional` 代替异常。
—
## 六、协程与并发的区别
– **协程**:单线程中模拟异步,避免多线程同步开销;
– **线程**:真正的并行执行,适合 CPU 密集型任务。
在实际项目中,常见做法是:使用协程处理 I/O 密集型部分,CPU 密集型使用线程池或 OpenMP。
—
## 七、实战案例:协程版 HTTP 服务器
“`cpp
#include
#include
#include
using boost::asio::ip::tcp;
using boost::asio::awaitable;
using boost::asio::use_awaitable;
namespace sys = boost::system;
awaitable
handle_request(tcp::socket socket) {
char data[1024];
std::size_t n = co_await socket.async_read_some(
boost::asio::buffer(data), use_awaitable);
std::string request(data, n);
// 简单响应
std::string response = “HTTP/1.1 200 OK\r\n”
“Content-Length: 13\r\n”
“Connection: close\r\n”
“\r\n”
“Hello, World!”;
co_await boost::asio::async_write(socket,
boost::asio::buffer(response),
use_awaitable);
socket.close();
}
awaitable
server(tcp::acceptor acceptor) {
for (;;) {
tcp::socket socket = co_await acceptor.async_accept(use_awaitable);
boost::asio::co_spawn(acceptor.get_executor(),
handle_request(std::move(socket)),
boost::asio::detached);
}
}
int main() {
boost::asio::io_context io{1};
tcp::acceptor acceptor(io, {tcp::v4(), 8080});
boost::asio::co_spawn(io, server(std::move(acceptor)), boost::asio::detached);
io.run();
}
“`
此例利用协程实现非阻塞 IO,代码结构清晰,易于维护。
—
## 八、学习路径建议
1. **基础语法**:先熟悉 `co_await`、`co_yield`、`co_return` 的用法。
2. **标准库**:掌握 `std::future`、`std::generator`、`std::suspend_always` 等。
3. **第三方库**:尝试 `boost::asio` 的协程接口,或 `cppcoro`、`awaitable` 等。
4. **实践项目**:从小型异步任务做起,逐步扩展到服务器、渲染管线或游戏逻辑。
—
## 九、常见陷阱与调试技巧
| 陷阱 | 说明 | 解决方案 |
|——|——|———-|
| 协程销毁前未 `resume` | 可能导致资源泄漏 | 确保每个协程在退出前已完成 `resume` 或 `destroy` |
| 递归协程 | 可能导致堆栈增长 | 采用尾递归优化或改写为迭代式生成器 |
| 异步异常 | `co_await` 产生异常时未捕获 | 使用 `try/catch` 包裹 `co_await`,或在 `promise_type::unhandled_exception` 中处理 |
| 性能调优 | `co_yield` 频繁拷贝 | 采用 `co_yield std::move(value)` 或返回引用 |
—
## 十、结语
C++20 的协程为语言带来了现代化的异步编程能力,让复杂的并发逻辑变得更加直观。通过理解协程的基本原理、熟悉关键语法以及实践常见用例,开发者可以在项目中更高效地处理 I/O、生成器以及状态机等任务。未来 C++23 及更高版本将进一步完善协程特性,值得持续关注。祝你在协程的旅程中收获满满!