C++20 引入了协程(coroutines),为异步编程提供了一套简洁而强大的语法。协程允许函数在执行过程中挂起并恢复,极大地提升了代码的可读性与性能。本文将从基本概念、协程的实现机制、常见用例到实战案例,系统阐述如何在实际项目中使用 C++20 协程。
1. 协程概念回顾
- 挂起(suspend): 协程可以在任意位置暂停执行,并将当前状态保存到协程框架。
- 恢复(resume): 在需要时重新激活协程,从挂起点继续执行。
- 协程返回值: 与普通函数不同,协程返回的是一个 协程对象(如 `generator ` 或 `task`),而非具体的值。
C++20 对协程的支持依赖于以下关键关键字:
| 关键字 | 作用 |
|---|---|
co_await |
暂停当前协程,等待一个可等待对象完成 |
co_yield |
产生一个值,并挂起协程,等到下次 resume 时继续 |
co_return |
结束协程并返回最终值(如果有) |
2. 协程的实现机制
2.1 协程句柄(std::coroutine_handle)
协程的底层由 std::coroutine_handle 管理。每个协程都有一个隐式生成的帧(frame),其中保存了协程状态、局部变量和调用栈信息。coroutine_handle 可以:
- 检查是否完成 (
done()) - 恢复协程 (
resume()) - 释放资源 (
destroy())
2.2 协程 promise
协程的返回类型需要提供一个 promise_type,用来定义协程的生命周期事件:
struct generator_promise {
using value_type = int; // 示例
generator get_return_object() {
return generator{std::coroutine_handle <generator_promise>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
std::suspend_always yield_value(int value) {
current_value = value; return {};
}
int current_value{};
};
2.3 关键生命周期方法
initial_suspend():协程开始时是否立即挂起。final_suspend():协程结束后是否挂起,允许执行清理代码。yield_value():处理co_yield,保存产出的值。
3. 常见协程类型
| 类型 | 典型用法 | 关键特性 |
|---|---|---|
| `generator | ||
| 生成序列 | 通过co_yield` 产生值 |
||
| `task | ||
| 异步任务 | 通过co_await` 等待 |
||
| `async_generator | ||
| ` | 异步生成器 | 结合异步 IO 使用 |
C++20 标准库中未提供完整实现,需要自己编写或使用第三方库(如 cppcoro、Microsoft Concurrency Runtime)。
4. 实战案例:异步文件读取
下面给出一个完整示例:使用协程读取文件内容,每行返回一次,演示协程的 co_await 与异步 IO 结合。
4.1 依赖
- C++20 编译器(GCC 10+ 或 Clang 10+)
asio(Boost.Asio 或 standalone Asio)支持异步文件 I/O
4.2 代码实现
#include <asio.hpp>
#include <iostream>
#include <string>
#include <vector>
#include <coroutine>
#include <optional>
#include <future>
using asio::awaitable;
using asio::co_spawn;
using asio::detached;
using asio::ip::tcp;
using namespace std::chrono_literals;
// 1. 读取一行文件的协程
awaitable<std::optional<std::string>> read_line(asio::random_access_handle& handle) {
const std::size_t buf_size = 1024;
std::vector <char> buffer(buf_size);
std::size_t pos = 0;
for (;;) {
std::size_t n = co_await handle.async_read_some(asio::buffer(buffer.data() + pos, buf_size - pos),
asio::use_awaitable);
if (n == 0) {
// EOF
if (pos == 0) co_return std::nullopt;
buffer.resize(pos);
break;
}
pos += n;
// 检测是否出现换行
if (std::find(buffer.begin(), buffer.begin() + pos, '\n') != buffer.begin() + pos) {
break;
}
if (pos == buf_size) {
// buffer full, but no newline yet
buffer.resize(buf_size * 2);
}
}
std::string line(buffer.data(), pos);
co_return line;
}
// 2. 主协程,读取文件所有行
awaitable <void> read_file(const std::string& path) {
asio::io_context io_context;
asio::random_access_handle handle(io_context);
co_await handle.open(path, std::ios::in);
while (auto line_opt = co_await read_line(handle)) {
std::cout << *line_opt << std::endl;
}
co_await handle.close();
}
// 3. 主入口
int main() {
asio::io_context io_context;
co_spawn(io_context, read_file("sample.txt"), detached);
io_context.run();
return 0;
}
说明:
asio::random_access_handle为异步文件句柄。awaitable为协程返回类型,表示异步操作。co_await在 I/O 完成前挂起协程。co_spawn用来启动协程任务。
5. 性能与调试
| 关注点 | 建议 |
|---|---|
| 堆栈占用 | 协程帧保存在堆上,避免深递归导致栈溢出 |
| 异常处理 | promise_type 中 unhandled_exception 必须妥善处理 |
| 调试难度 | 采用 -g 调试,使用 gdb/lldb 的 thread apply all bt 查看协程堆栈 |
6. 与现有项目的集成
- 逐步迁移:先在新模块使用协程,逐步把同步函数改写为协程版。
- 封装统一:为常用异步 I/O(网络、文件、数据库)创建统一的协程包装。
- 错误码:结合
std::expected或自定义错误类型,在协程中统一处理错误。
7. 常见陷阱
- 忘记
co_await:直接返回一个awaitable对象会导致挂起点不正确。 - promise_type 与返回类型不匹配:
get_return_object()必须返回与协程返回类型匹配的对象。 - 资源泄露:
std::coroutine_handle需要手动destroy(),否则会泄露。
8. 进一步阅读
- 《C++20 协程详解》 — 详尽剖析协程实现。
- 《Boost.Asio 与 C++20 协程》 — 结合实际网络编程案例。
- 《cppcoro》GitHub 项目 — 提供
generator,task等实用实现。
结语
C++20 协程为语言提供了强大的异步编程模型。通过掌握其基本语法、生命周期与常见实现,可以让代码更简洁、并发更高效。希望本文的示例与技巧能帮助你在项目中顺利使用协程,开启 C++ 异步编程的新篇章。