C++20 中的协程:从概念到实践

协程(coroutine)是 C++20 引入的一项强大特性,它让我们能够编写非阻塞、可暂停的函数,从而简化异步编程、生成器以及复杂的控制流。本文将从协程的基本概念、语法实现、常见用例以及性能考量等方面进行系统阐述,帮助读者快速掌握并在项目中实际应用。


一、协程的基本概念

  1. 可暂停函数
    与传统函数一次性完成所有工作不同,协程可以在执行过程中“挂起”(co_awaitco_yieldco_return),让调用方在需要时继续执行。
  2. 状态机
    协程内部被编译器转换为一个隐式的状态机,每一次挂起点对应一个状态,协程的状态由 std::coroutine_handle 管理。
  3. 协程句柄
    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 及更高版本将进一步完善协程特性,值得持续关注。祝你在协程的旅程中收获满满!

发表评论