C++20 在语言层面引入了协程(Coroutine)这一特性,使得异步编程的语法与实现方式得以大幅简化。本文从协程的基本概念出发,逐步展开对其实现机制、标准库支持以及实际项目中的应用场景进行剖析,并提供一段完整的示例代码,帮助读者快速上手。
1. 协程的基本概念
协程是一种可挂起的函数,它可以在执行过程中暂停并保留状态,随后再恢复执行。与线程不同,协程的切换开销极低,调度完全由程序控制。C++20 的协程通过三大核心关键字实现:
co_await:挂起协程并等待一个异步事件完成;co_yield:生成一个值并挂起协程;co_return:返回协程的最终结果并结束协程。
协程的入口函数需要返回 `std::future
`、`std::generator` 或自定义的 Promise 结构。 — #### 2. 协程实现的底层机制 ##### 2.1 Promise 与 Awaiter C++ 协程的工作流程如下: 1. **生成器创建**:编译器在调用协程入口函数时,先创建一个 Promise 对象并在堆上分配协程的状态机(即 `promise_type`)。 2. **`initial_suspend`**:协程立即调用 `initial_suspend()`,返回 `std::suspend_always` 或 `std::suspend_never`,决定协程是否立即挂起。 3. **执行主体**:从 `initial_suspend` 开始执行协程主体,遇到 `co_await` 时会调用对应 Awaiter 的 `await_ready`、`await_suspend`、`await_resume`。 4. **挂起与恢复**:`await_suspend` 可以把协程挂起,将控制权转移给外部调度器或事件循环;当事件完成后,协程通过 `await_resume` 恢复并继续执行。 5. **`final_suspend`**:协程结束后会调用 `final_suspend()`,协程可以在此时被销毁或继续挂起。 ##### 2.2 代码生成与优化 编译器在幕后为协程生成一个状态机结构体,该结构体保存所有局部变量和状态机的程序计数器。由于协程的挂起点是已知的,编译器可以对状态机进行静态分析,从而实现类似 `switch` 或 `goto` 的跳转逻辑。进一步的优化包括: – **逃逸分析**:若协程在调用方立即使用,编译器可以把 Promise 对象直接放在调用栈上,避免堆分配。 – **尾部优化**:若 `co_return` 是协程的最后一条语句,编译器可将其与 `final_suspend` 合并,减少一次分配与析构。 — #### 3. 标准库与第三方协程支持 | 组件 | 说明 | |——|——| | `std::future ` | 基础异步返回值。| | `std::generator ` | 用于生成序列的协程。 | | `std::task `(在 TS 中) | 现代异步任务,支持 `co_await`。| | `cppcoro` | 一套第三方协程工具库,提供高性能的协程调度器。 | | `asio::awaitable ` | Boost.Asio 提供的协程包装,用于网络编程。| 在实际项目中,许多异步 I/O 框架(如 Boost.Asio、gRPC、zlib)都已将协程纳入其异步模型中,进一步提升代码可读性与性能。 — #### 4. 实际项目中的协程使用示例 以下示例演示如何使用 `std::future` 和 `std::generator`,配合自定义 `sleep` Awaiter,完成一个简易的异步任务调度器。 “`cpp #include #include #include #include #include #include #include struct SleepAwaiter { std::chrono::milliseconds dur; bool await_ready() noexcept { return dur.count() <= 0; } void await_suspend(std::coroutine_handle h) noexcept { std::thread([h, dur=dur]{ std::this_thread::sleep_for(dur); h.resume(); }).detach(); } void await_resume() noexcept {} }; auto sleep_for(std::chrono::milliseconds ms) { return SleepAwaiter{ms}; } struct AsyncTask { struct promise_type { std::future get_return_object() { return std::future {handle}; } std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void unhandled_exception() { std::terminate(); } void return_value(int value) noexcept { value_ = value; } int value_; std::coroutine_handle handle; }; std::future fut; AsyncTask(std::future f) : fut(std::move(f)) {} AsyncTask(const AsyncTask&) = delete; AsyncTask(AsyncTask&&) = delete; AsyncTask& operator=(const AsyncTask&) = delete; AsyncTask& operator=(AsyncTask&&) = delete; ~AsyncTask() = default; }; AsyncTask asyncAdd(int a, int b) { co_await sleep_for(std::chrono::milliseconds(500)); co_return a + b; } struct Generator { struct promise_type { std::optional current; std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void unhandled_exception() { std::terminate(); } std::generator get_return_object() { return { std::coroutine_handle::from_promise(*this) }; } std::suspend_always yield_value(int value) noexcept { current = value; return {}; } void return_void() noexcept {} }; }; Generator countUpTo(int n) { for (int i = 0; i < n; ++i) { co_yield i; } } int main() { // Async task example auto task = asyncAdd(3, 7); std::cout << "Computing asyncAdd(3, 7)…\n"; std::cout << "Result: " << task.fut.get() << '\n'; // Generator example std::cout << "Generating numbers 0..4:\n"; for (int v : countUpTo(5)) { std::cout << v << ' '; } std::cout << '\n'; return 0; } “` **说明**: – `sleep_for` 通过自定义 `SleepAwaiter` 实现异步延迟,避免阻塞主线程; – `AsyncTask` 使用 `std::future ` 作为返回类型,模拟异步计算; – `Generator` 展示了 `std::generator` 的用法,返回可迭代的序列。 — #### 5. 协程使用的注意事项 1. **异常传播**:协程内部抛出的异常会传递到 Promise 的 `unhandled_exception()`,若未处理则导致程序终止,建议在 Promise 中捕获并封装到 `std::future` 的异常状态。 2. **资源管理**:协程内分配的资源(如动态内存、文件句柄)在协程结束后必须及时释放,最好使用 RAII。 3. **调度器选择**:在高并发环境下,单线程事件循环(如 `asio::io_context`)足以处理协程调度,避免多线程竞争导致的上下文切换开销。 4. **与现有库兼容**:若项目已使用 `std::async` 或传统回调,迁移到协程需要逐步重构,保持接口一致性。 — #### 6. 结语 C++20 的协程技术为异步编程提供了一个自然、优雅的语法层面解决方案。它不但降低了回调地狱的风险,还让异步代码更接近同步代码的可读性。随着协程在标准库中的完善以及第三方生态的快速发展,未来 C++ 项目将更加倾向于使用协程来处理 I/O、网络、游戏循环等需要高并发的场景。希望本文能帮助你快速了解协程的实现原理与实际应用,为你的项目带来更高的性能与可维护性。