在 C++20 之前,异步编程往往依赖于回调、Future/Promise 或第三方库(如 Boost.Asio、libuv)。这些方案虽然功能强大,却在语法和可读性上都有一定的门槛。C++20 引入了协程(Coroutines)语法,为编写顺序化、可读性高的异步代码提供了新的手段。本文将从协程的基本概念、关键语法、编译实现以及实际案例三方面,系统介绍 C++20 协程的使用与实现原理。
1. 协程的基本概念
协程是一种轻量级的子例程,能够在函数内部挂起(co_await/co_yield/co_return)并在后续恢复执行。与线程不同,协程的切换由程序显式控制,避免了线程上下文切换的昂贵开销。协程的执行状态保存在栈上,编译器把协程拆分成若干个状态机的“步骤”,在挂起点将当前状态存储在堆或寄存器中,以便后续恢复。
2. 关键语法
| 关键词 | 作用 | 示例 |
|---|---|---|
co_await |
挂起协程,等待 awaitable 对象完成 | int value = co_await asyncRead(); |
co_yield |
产生一个值并挂起协程 | co_yield value; |
co_return |
结束协程并返回最终结果 | co_return finalValue; |
co_await std::suspend_always / std::suspend_never |
显式挂起/不挂起 | co_await std::suspend_always(); |
协程的返回类型通常是 `std::future
`、`std::generator` 或者自定义的 awaitable 对象。标准库提供了两类核心类型: – **`std::future `**:支持异步计算,使用 `co_return` 返回最终值。 – **`std::generator `**:支持值序列,使用 `co_yield` 逐步产生值。 ### awaitable 对象 要在 `co_await` 中使用对象,该对象必须满足 **Awaitable** 需求。最基本的实现方式是: “`cpp struct MyAwaitable { bool await_ready() const noexcept { return false; } void await_suspend(std::coroutine_handle h) const noexcept { // 注册回调,在完成时恢复 h } int await_resume() const noexcept { return 42; } }; “` – `await_ready` 判断是否已经完成;若返回 `true`,协程会立即继续。 – `await_suspend` 负责挂起协程,并在异步操作完成后恢复。 – `await_resume` 在恢复后返回给协程的值。 — ## 3. 编译器实现细节 ### 状态机生成 编译器把协程函数拆分成若干“块”,每个挂起点产生一个状态。内部使用 `std::coroutine_handle` 管理协程句柄,并把执行上下文(局部变量、返回地址)压栈到协程的栈帧。 ### 协程句柄与堆管理 协程对象在堆上分配(通常通过 `operator new`),而返回给调用者的是一个 `std::coroutine_handle` 或包装类型(如 `std::future`)。当协程完成后,资源会被 `std::coroutine_handle::destroy()` 自动回收,或者手动调用。 ### 性能优化 – **`noexcept`**:所有 awaitable 成员尽量声明为 `noexcept`,以避免异常传播成本。 – **分配优化**:`std::pmr::memory_resource` 可用于定制协程的内存池,减少堆碎片。 – **无状态 awaitable**:如果 awaitable 无需维护状态,可以用 `std::suspend_always` 或 `std::suspend_never` 作为占位符,避免额外对象。 — ## 4. 实际案例:异步文件读取 下面演示一个简化的异步文件读取示例,使用 `asio`(Boost.Asio 的 C++20 协程适配): “`cpp #include #include #include #include using asio::awaitable; using asio::buffer; using asio::co_spawn; using asio::detached; using asio::io_context; using asio::streambuf; awaitable asyncReadFile(io_context& io, const std::string& path) { std::ifstream file(path, std::ios::binary | std::ios::ate); if (!file) co_return std::string(); // 读取失败 std::streamsize size = file.tellg(); file.seekg(0, std::ios::beg); std::string buffer(static_cast (size), ‘\0’); co_await asio::async_read(file, asio::buffer(buffer), asio::use_awaitable); co_return buffer; } int main() { io_context io; co_spawn(io, asyncReadFile(io, “example.txt”), [](std::string result) { if (!result.empty()) std::cout ` 的协程,内部使用 `co_await` 等待 `asio::async_read` 完成。 2. `co_spawn` 用于在事件循环中启动协程,回调函数处理读取结果。 3. 由于 `asio::async_read` 本身返回一个 awaitable,协程能够顺序编写,代码易于阅读。 — ## 5. 协程与线程的比较 | 特点 | 协程 | 线程 | |——|——|——| | 调度 | 由程序显式挂起/恢复 | 由调度器自动 | | 开销 | 轻量级,仅状态机切换 | 上下文切换昂贵 | | 并发模型 | 单线程异步 | 多线程并发 | | 适用场景 | I/O 密集、顺序逻辑 | CPU 密集、需要真正并行 | 在 I/O 密集型场景,协程往往比多线程更高效;在 CPU 计算密集型任务,传统多线程仍然更合适。 — ## 6. 未来展望 C++23 继续扩展协程功能,加入了 `std::generator` 的 `operator*`、`operator++` 等更直观的遍历接口,并改进了 awaitable 的异常处理。同时,标准化的协程适配器(如 `std::task`)将进一步简化异步编程。结合现代编译器的更好优化,C++ 协程在高性能服务器、游戏引擎、实时数据处理等领域的应用前景非常广阔。 — ### 小结 C++20 的协程为开发者提供了一种既高效又易读的异步编程方式。掌握其基本语法、awaitable 的实现以及编译器的状态机机制,能够在实际项目中快速构建顺序化的异步逻辑,显著提升代码可维护性与执行性能。随着标准的演进和工具链的完善,协程无疑将成为 C++ 生态中不可或缺的重要特性。