C++20引入了协程(coroutines)作为语言级特性,极大简化了异步编程的实现。相比传统的回调、线程或状态机,协程以“直观且可组合”的方式表达异步流程。本文将从协程的基本概念、实现原理,到实际使用示例(异步文件读取)展开说明,并给出最佳实践与常见坑。
一、协程的基本概念
| 关键词 | 含义 |
|---|---|
| 协程 | 可以在任意位置挂起并在未来恢复的函数。 |
co_await |
表示等待一个协程返回,挂起当前协程直到等待对象完成。 |
co_yield |
产生一个值并挂起,支持生成器模式。 |
co_return |
结束协程,返回最终结果。 |
awaitable |
对象必须实现 await_ready(), await_suspend(), await_resume() 三个接口,才能被 co_await。 |
协程在编译时会被展开为普通的状态机,await_suspend 用于决定是否挂起;若挂起,协程会在外部事件完成后被恢复。
二、实现原理概览
- 生成状态机:编译器把
co_await/co_yield的位置生成状态机状态。每一次挂起,状态机将当前局部变量保存在堆上或在寄存器中。 promise_type:每个协程都有对应的promise_type,负责:- 返回值:
get_return_object()产生协程句柄。 - 异常处理:
unhandled_exception()。 - 挂起/恢复:
initial_suspend()、final_suspend()。
- 返回值:
- 协程句柄:`std::coroutine_handle
` 可以用来手动恢复、检查状态或销毁协程。
三、示例:异步文件读取
以下示例演示如何使用协程与 asio 结合实现异步读取文件内容。asio 提供了对 I/O 事件的 awaitable 包装。
#include <iostream>
#include <asio.hpp>
#include <fstream>
#include <string>
#include <vector>
namespace asio = boost::asio;
// 把标准文件流包装成 awaitable
asio::awaitable<std::string> async_read_file(asio::io_context& ctx, const std::string& path) {
using namespace std::literals;
// 异步打开文件
std::error_code ec;
std::fstream file(path, std::ios::binary | std::ios::in);
if (!file) {
co_return ""; // 失败返回空字符串
}
// 读取文件内容
std::vector <char> buffer((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
std::string content(buffer.begin(), buffer.end());
co_return content;
}
// 主协程:并发读取两个文件
asio::awaitable <void> main_coro(asio::io_context& ctx) {
std::string content1 = co_await async_read_file(ctx, "file1.txt");
std::string content2 = co_await async_read_file(ctx, "file2.txt");
std::cout << "File1 size: " << content1.size() << " bytes\n";
std::cout << "File2 size: " << content2.size() << " bytes\n";
}
int main() {
asio::io_context ctx;
// 启动协程
std::make_shared<asio::spawn_handler>(ctx, [](asio::coroutine_handle<> h) {
main_coro(ctx).resume();
});
ctx.run();
}
说明
async_read_file实际上没有真正的异步 I/O,文件读取是同步完成的。若要真正异步读取,需使用系统的异步 I/O API(如 Linux 的io_uring或 Windows 的ReadFileEx)包装成awaitable。- 示例演示了协程之间的组合与
co_await的使用,真正的异步 I/O 只需替换读取逻辑即可。
四、最佳实践
| 经验 | 说明 |
|---|---|
| **尽量返回 `awaitable | |
** | 保持函数签名清晰,调用方可直接co_await`。 |
|
| 避免在协程内部创建大量临时对象 | 协程挂起时局部变量会被保存,若对象过大会增加堆开销。 |
使用 asio::co_spawn |
统一协程启动与错误处理,避免手动管理句柄。 |
| 正确处理异常 | promise_type::unhandled_exception 应将异常转发到协程句柄或外层。 |
考虑 await_ready 的实现 |
对于同步完成的操作,可在 await_ready 返回 true 以避免挂起。 |
五、常见坑与调试技巧
| 坑 | 对策 |
|---|---|
忘记 co_return |
编译器会报“expected ‘co_return’”错误。 |
| 协程对象在栈上失效 | 确保协程句柄存活到 io_context.run() 完成;最好通过 std::shared_ptr 持有。 |
| 异常未捕获 | 在协程入口使用 try/catch 包裹,或在 promise_type::unhandled_exception 中处理。 |
| 协程挂起后不恢复 | 检查 await_suspend 返回 true(挂起)与 false(不挂起)的逻辑。 |
| 调试输出混乱 | 使用 asio::detail::debug_output 或 spdlog,并在 io_context 的 io_service::strand 上同步输出。 |
六、总结
C++20 协程为异步编程提供了更直观、可组合的语义,降低了回调地狱的风险。配合成熟的 I/O 框架(如 asio)可以实现高性能、可维护的网络或文件处理。掌握 awaitable 接口、状态机展开机制与协程句柄管理,是成为现代 C++ 开发者的关键技能。祝你在协程的世界里编写出流畅且高效的异步程序!