协程在 C++20 中的演进与实际应用

在 C++20 之前,异步编程往往依赖回调、Future/Promise 或第三方库(如 Boost.Asio、libuv 等)。随着 C++20 的发布,协程(coroutine)成为语言核心特性,为写出更直观、可读性更高的异步代码提供了强大工具。本文从协程的基本概念入手,梳理其在 C++20 以及后续版本中的演进,并给出一个完整的示例,展示如何在 C++ 中实现一个简易的异步任务调度器。

1. 协程基础概念

协程是一种能够在执行过程中挂起(yield)并在需要时恢复的函数。C++ 中的协程主要依赖于以下三个关键组成:

  1. co_await:等待一个 awaitable 对象完成,挂起当前协程。
  2. co_yield:生成一个值并挂起协程,等待下一次请求。
  3. co_return:返回最终结果并结束协程。

协程的实现通过一个协程句柄(coroutine_handle)来管理其生命周期。协程的返回类型不一定是普通值,而是一个promise对象,该对象描述协程如何传递结果、如何处理异常以及如何在挂起点恢复。

2. C++20 之前的异步编程

  • 回调函数:最早的异步模式,缺点是回调嵌套导致“回调地狱”。
  • Future/Promise:C++11 标准库提供了异步结果容器,但缺少真正的挂起语义,导致需要频繁轮询或阻塞等待。
  • 第三方库:如 Boost.Asio 提供了事件循环与异步 I/O,但 API 仍然较为繁琐。

这些方法都无法像协程一样在编译时保证挂起点的合法性,也不方便在业务代码中嵌入同步与异步逻辑。

3. C++20 协程的核心 API

3.1 std::generator

C++20 标准库引入了 `std::generator

`,它是基于协程实现的可迭代容器。使用方式: “`cpp std::generator range(int start, int end) { for (int i = start; i < end; ++i) co_yield i; } “` 通过 `range(0, 5)` 可以得到 0~4 的序列,内部实现为协程,使用 `co_yield` 生成每个值。 ### 3.2 `std::task` 在 C++23 标准中,`std::task ` 定义为一个异步任务类型,支持 `co_await`。它与 `std::future` 的区别在于: – **非阻塞**:`co_await` 直接挂起,不会阻塞线程。 – **可组合**:可以在协程内部链式调用其他 `std::task`。 ### 3.3 `co_await` 的实现细节 `co_await` 关键字对 awaitable 对象调用 `await_ready`、`await_suspend`、`await_resume` 三个成员函数。标准库提供了默认实现,但我们可以自定义 Promise 类型来改变挂起行为,例如实现一个简单的事件循环。 ## 4. 设计一个简易的协程任务调度器 下面给出一个完整的代码示例,实现一个最小化的协程调度器,支持: – 定义 `Task` 类型(返回 `int`)。 – 支持 `co_await` 以等待 `Task` 完成。 – 使用 `std::queue` 存储待调度的协程句柄。 “`cpp #include #include #include #include #include struct Task { struct promise_type { int value_; Task get_return_object() { return Task{std::coroutine_handle ::from_promise(*this)}; } std::suspend_always initial_suspend() { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void unhandled_exception() { std::terminate(); } void return_value(int v) { value_ = v; } }; std::coroutine_handle handle_; Task(std::coroutine_handle h) : handle_(h) {} ~Task() { if (handle_) handle_.destroy(); } int get() { handle_.resume(); return handle_.promise().value_; } }; using namespace std::chrono_literals; // 一个模拟异步 I/O 的协程 Task async_sleep(int ms) { std::this_thread::sleep_for(msms); co_return ms; // 返回等待的毫秒数 } // 简易调度器 class Scheduler { std::queue<std::coroutine_handle> tasks_; public: void spawn(Task t) { tasks_.push(t.handle_); } void run() { while (!tasks_.empty()) { auto h = tasks_.front(); tasks_.pop(); h.resume(); if (!h.done()) tasks_.push(h); // 若未结束则重新入队 } } }; int main() { Scheduler sched; Task t1 = async_sleep(1000); Task t2 = async_sleep(500); sched.spawn(t1); sched.spawn(t2); sched.run(); std::cout << "t1 finished with: " << t1.get() << "ms\n"; std::cout << "t2 finished with: " << t2.get() << "ms\n"; } “` ### 代码说明 1. **Task**:封装协程句柄,提供 `get()` 方法获取最终返回值。`promise_type` 的 `final_suspend` 返回 `suspend_always`,让调度器自行决定是否再次挂起。 2. **async_sleep**:模拟异步 I/O,实际是阻塞 `sleep_for`,但在真实环境中可替换为非阻塞操作。 3. **Scheduler**:管理协程句柄队列。每次 `resume()` 后检查 `done()`,未结束则重新入队。实现了一个极简的事件循环。 ## 5. 协程的优势与注意事项 – **可读性**:代码结构类似同步写法,减少嵌套与回调。 – **性能**:协程不需要栈拷贝,挂起点仅保存必要状态,内存占用低。 – **错误处理**:异常可以在 Promise 层统一捕获,避免遗漏。 – **兼容性**:需要 C++20 及以上编译器支持,GCC/Clang/MSVC 已经稳定实现。 但也有需要注意的地方: – **堆栈大小**:虽然协程轻量,但在每次挂起时会保存局部变量,需要合理管理。 – **多线程**:协程默认不线程安全,若在多线程中共享句柄,需同步。 – **调试**:调试器对协程的支持仍在完善,单步调试可能出现跳过帧的问题。 ## 6. 结语 C++20 的协程为异步编程提供了真正的语义化支持,使得我们可以在同一语言层面编写同步与异步逻辑,极大提升代码可维护性。随着 C++23 对 `std::task` 的完善,协程将在更大范围内得到推广。希望通过本文的示例,能让你快速上手并在项目中尝试协程带来的好处。</std::coroutine_handle

发表评论