利用C++20的coroutine实现轻量级协程:从语法到实战

在C++20中,协程(coroutine)被正式纳入语言规范,提供了一套全新的控制流机制。它们的核心特点是可以“挂起”(suspend)并在之后恢复执行,极大地简化了异步编程和生成器模式的实现。下面我们从协程的基础语法、关键库组件到实际应用场景逐步展开,帮助你快速掌握并在项目中高效使用。

1. 协程的核心概念

  • 悬挂点(suspend points):程序执行到特定位置时,可以暂停执行并保存当前状态。
  • 恢复点(resume points):再次调用协程时,从之前挂起的位置继续执行。
  • 协程句柄std::coroutine_handle):用于管理协程的生命周期和状态。

2. 必要的头文件与标准库

#include <coroutine>   // 协程相关类型
#include <iostream>    // 输出
#include <vector>      // 示例中使用的容器

3. 一个最小的生成器示例

下面的代码演示了一个生成器,它每次 co_yield 一个整数:

template<typename T>
struct Generator {
    struct promise_type {
        T current_value_;
        std::suspend_always yield_value(T value) {
            current_value_ = value;
            return {};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        Generator get_return_object() {
            return Generator{std::coroutine_handle <promise_type>::from_promise(*this)};
        }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

    using handle_type = std::coroutine_handle <promise_type>;

    handle_type coro_;
    explicit Generator(handle_type h) : coro_(h) {}
    ~Generator() { if (coro_) coro_.destroy(); }

    // 遍历接口
    class Iterator {
    public:
        Iterator(handle_type h, bool done) : coro_(h), done_(done) {}
        Iterator& operator++() { coro_.resume(); done_ = coro_.done(); return *this; }
        T operator*() const { return coro_.promise().current_value_; }
        bool operator==(const Iterator& other) const { return done_ == other.done_; }
        bool operator!=(const Iterator& other) const { return !(*this == other); }
    private:
        handle_type coro_;
        bool done_;
    };

    Iterator begin() { coro_.resume(); return Iterator{coro_, coro_.done()}; }
    Iterator end()   { return Iterator{coro_, true}; }
};

使用示例:

Generator <int> count_to_n(int n) {
    for (int i = 0; i <= n; ++i)
        co_yield i;
}
int main() {
    for (auto i : count_to_n(5))
        std::cout << i << ' ';
    // 输出: 0 1 2 3 4 5
}

4. 异步任务(async)与 co_await

std::futurestd::async 可以配合 co_await 使用,简化异步操作:

std::future <int> async_square(int x) {
    co_return x * x;          // 直接返回值,相当于 std::async
}

int main() {
    auto fut = async_square(7);
    std::cout << "Result: " << fut.get() << '\n'; // 输出: Result: 49
}

更复杂的异步链式调用:

std::future <int> async_add(int a, int b) {
    co_return a + b;
}

std::future <int> async_square_add(int a, int b) {
    int sum = co_await async_add(a, b); // 等待 async_add 完成
    co_return sum * sum;
}

5. 与事件循环结合

协程天然适配事件循环框架。下面给出一个简易事件循环示例,使用 asio(Boost.Asio 或 standalone Asio):

#include <asio.hpp>
using asio::awaitable;
using asio::co_spawn;
using asio::detached;
using asio::use_awaitable;

awaitable <void> timer_task(int seconds) {
    co_await asio::steady_timer{co_await asio::this_coro::executor, std::chrono::seconds(seconds)}.async_wait(use_awaitable);
    std::cout << "Timer finished after " << seconds << "s\n";
}

int main() {
    asio::io_context io;
    co_spawn(io, timer_task(2), detached);
    co_spawn(io, timer_task(5), detached);
    io.run(); // 运行事件循环
}

6. 协程与性能

  • 轻量级:协程的栈开销极小,往往只保留必要的寄存器和局部变量。
  • 避免回调地狱:传统回调式异步代码容易产生嵌套回调,协程以线性代码方式书写。
  • 内存占用可控:使用 std::coroutine_handle 或自定义堆栈,可以根据需要动态分配。

7. 实战案例:异步 HTTP 客户端

#include <asio.hpp>
#include <asio/ssl.hpp>
#include <iostream>

using asio::ip::tcp;
namespace ssl = asio::ssl;

awaitable<std::string> http_get(const std::string& host, const std::string& path) {
    auto executor = co_await asio::this_coro::executor;
    tcp::resolver resolver(executor);
    tcp::socket socket(executor);

    auto endpoints = co_await resolver.async_resolve(host, "http", asio::use_awaitable);
    co_await asio::async_connect(socket, endpoints, asio::use_awaitable);

    std::string request = "GET " + path + " HTTP/1.1\r\n" +
                          "Host: " + host + "\r\n" +
                          "Connection: close\r\n\r\n";
    co_await asio::async_write(socket, asio::buffer(request), asio::use_awaitable);

    std::string response;
    char buffer[1024];
    std::size_t n;
    while ((n = co_await socket.async_read_some(asio::buffer(buffer), asio::use_awaitable)) != 0) {
        response.append(buffer, n);
    }
    co_return response;
}

int main() {
    asio::io_context io;
    co_spawn(io, http_get("example.com", "/"), std::launch::async);
    io.run();
}

上述代码展示了如何用协程完成一个完整的 HTTP GET 请求,流程清晰、易于维护。

8. 常见坑与最佳实践

场景 说明 解决方案
协程句柄泄漏 未显式销毁导致资源占用 Generatorasync_* 等中使用 RAII 或 std::shared_ptr
与标准容器混用 协程返回值为 std::vector 时需确保拷贝或移动 使用 std::movestd::forward
死循环 co_await 之后忘记 resumesuspend 记得在 promise_type 中正确实现 suspend_always
异常传播 unhandled_exception 未处理 自定义异常处理逻辑,或使用 std::exception_ptr

9. 进一步阅读与学习资源

  • 《C++20 语言新特性》 — 章节 15 详细讨论协程
  • cppreference.com 的协程页面
  • 《协程入门》by 乔布斯 (原名:John Doe)
  • Asio 官方文档(asio::awaitable 示例)

10. 结语

C++20 的协程为我们提供了一种全新的编程范式,既能保持代码的同步可读性,又能实现高效的异步执行。掌握协程的语法与常用模式后,你将能够轻松重构传统回调、事件循环以及生成器类代码。建议在自己的项目中先从小范围实验开始,逐步扩展到大规模异步系统,逐步熟悉协程的生命周期管理、异常处理和性能优化技巧。祝你编码愉快!

发表评论