C++20 中的协程:轻松实现异步编程

协程(coroutine)是 C++20 标准中一次性让你在同一线程中实现异步、事件驱动逻辑的强大工具。与传统的回调、Future 或线程相比,协程语法简洁、执行效率更高,也不需要手动管理线程池。本文从基本概念、语法结构到实际案例,带你快速入门 C++20 协程。

1. 协程的基本概念

协程是一种特殊的函数,它可以在执行过程中“挂起”(suspend)并在之后恢复(resume)。挂起点是协程的暂停点(co_await, co_yield, co_return),恢复点则是再次执行协程时从挂起点继续。协程的优势在于:

  • 异步代码写作像同步:使用 co_await 可以像 await 一样等待异步操作完成,而不需要写回调链。
  • 资源共享:协程的状态(即执行栈)由编译器自动拆分为多个状态机,存放在堆或栈上,轻量高效。
  • 非阻塞:协程挂起时不会占用线程,只在需要时恢复。

2. 协程的关键类型

协程需要定义返回类型,最常见的两种是:

  • `std::future `:用于单个异步结果,适用于需要与其他异步操作组合的场景。
  • `generator `(来自 `cppcoro` 或手写):生成器式协程,使用 `co_yield` 产生值流。

此外,还需要定义 promise type,协程编译器通过 promise_type 进行状态管理。你可以自定义 promise_type,甚至实现自定义协程库。

3. 写一个简单的异步函数

#include <coroutine>
#include <iostream>
#include <chrono>
#include <thread>

struct Task {
    struct promise_type {
        Task get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { std::terminate(); }
        void return_void() {}
    };
};

Task async_print(int id) {
    std::cout << "Task " << id << " started\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Task " << id << " finished\n";
    co_return;
}

int main() {
    async_print(42);  // 协程立即执行到挂起点
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 主线程等待
}

这里 Task 使用 std::suspend_alwaysfinal_suspend 处挂起,确保协程结束后被销毁。虽然 async_print 在主线程内执行,但你可以在 co_await 处挂起等待异步事件。

4. 结合 co_awaitstd::future

C++20 引入 std::future 与协程的天然集成。以下示例展示了如何在协程中等待 std::future

#include <coroutine>
#include <future>
#include <iostream>
#include <thread>

Task wait_future(std::future <void> fut) {
    co_await fut;  // 等待 future 完成
    std::cout << "Future completed\n";
}

int main() {
    auto fut = std::async(std::launch::async, []{
        std::this_thread::sleep_for(std::chrono::seconds(1));
    });
    wait_future(std::move(fut));
    std::this_thread::sleep_for(std::chrono::seconds(2));
}

协程在 co_await fut 处挂起,直到 fut 完成。内部实现使用 std::experimental::coroutine_traits 自动推断,极大简化异步流程。

5. 生成器式协程示例

生成器可以用来一次性生成一系列值,而不需要显式存储整个容器:

#include <generator>
#include <iostream>

std::generator <int> range(int start, int end) {
    for (int i = start; i < end; ++i) {
        co_yield i;
    }
}

int main() {
    for (int n : range(0, 5)) {
        std::cout << n << ' ';
    }
}

生成器实现非常轻量,适用于文件行读取、数据流处理等场景。

6. 性能与注意事项

  • 协程的堆分配:默认实现可能在堆上分配协程对象,若频繁创建会产生开销。可使用 std::coroutine_handle 进行手动内存管理。
  • 错误传播:在协程中使用 throw 时,异常会被 promise_type::unhandled_exception 捕获,默认行为是 std::terminate。可以自定义异常处理逻辑。
  • 调试困难:由于编译器拆分协程,调试会话可能出现跳过代码。使用 IDE 的协程调试插件可以缓解。

7. 结束语

C++20 的协程让异步编程从“手写回调链”跃升为“可读可维护”。通过掌握 co_awaitco_yield 与自定义 promise_type,你可以轻松实现网络 I/O、并发任务、流式数据处理等高级功能。下一步建议结合网络库(如 asio)和数据库驱动,构建完整的异步框架。祝你编码愉快!

发表评论