C++20 协程的实战应用与最佳实践

在 C++20 中,协程(coroutines)为异步编程提供了语言级支持,极大简化了异步代码的书写。本文将从基本概念入手,展示如何在实际项目中使用协程实现高效、可维护的异步逻辑,并给出常见坑及解决方案。

一、协程基本概念

  • 协程:一种可挂起、恢复的函数。
  • promise_type:协程的承诺类型,负责管理协程状态。
  • generator:最常见的协程形式,用于生成一系列值。

1.1 协程的启动与挂起

std::generator <int> counter(int n) {
    for (int i = 0; i < n; ++i) co_yield i;   // co_yield:挂起并返回一个值
}

1.2 promise_type 示例

struct counter_promise {
    int current{};
    auto get_return_object() { return std::generator <int>{*this}; }
    auto initial_suspend() { return std::suspend_always{}; }
    auto final_suspend()   { return std::suspend_always{}; }
    void unhandled_exception() { std::terminate(); }
    void return_void() {}
    auto yield_value(int val) {
        current = val;
        return std::suspend_always{};
    }
};

二、协程在 I/O 中的应用

协程可以配合 std::experimental::async 或者自定义 I/O 事件循环实现非阻塞 I/O。
下面以 TCP 服务器为例,展示如何使用协程实现无回调链。

#include <iostream>
#include <boost/asio.hpp>
#include <coroutine>
#include <string>

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

class async_read {
public:
    struct promise_type;
    using handle_type = std::coroutine_handle <promise_type>;
    std::string result;

    async_read(handle_type h) : coro(h) {}
    async_read(const async_read&) = delete;
    async_read(async_read&& rhs) noexcept : coro(rhs.coro) { rhs.coro = nullptr; }
    ~async_read() { if (coro) coro.destroy(); }

    struct promise_type {
        std::string value;
        std::coroutine_handle<> continuation;

        async_read get_return_object() {
            return async_read{handle_type::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept {
            if (continuation) continuation.resume();
            return {};
        }
        void return_value(std::string val) { value = std::move(val); }
        void unhandled_exception() { std::terminate(); }
    };

    bool await_ready() { return false; }
    void await_suspend(std::coroutine_handle<> cont) {
        continuation = cont;
    }
    std::string await_resume() { return std::move(result); }
};

async_read read_from_socket(tcp::socket& sock) {
    asio::streambuf buf;
    std::size_t n = co_await asio::async_read(sock, buf, asio::use_awaitable);
    std::istream is(&buf);
    std::string data((std::istreambuf_iterator <char>(is)),
                      std::istreambuf_iterator <char>());
    co_return data;
}

int main() {
    asio::io_context io;
    tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 8080));
    tcp::socket sock(io);
    acceptor.async_accept(sock, [&](const boost::system::error_code& ec) {
        if (!ec) {
            asio::co_spawn(io, [&]() -> asio::awaitable <void> {
                std::string data = co_await read_from_socket(sock);
                std::cout << "Received: " << data << '\n';
            }, asio::detached);
        }
    });
    io.run();
}

关键点

  • asio::use_awaitable 让 ASIO 与 C++20 协程无缝协作。
  • async_read 用作协程包装器,内部维护继续点 continuation

三、协程与异常处理

协程中异常的传播遵循 promise_type::unhandled_exception 的规则。若需要在协程内部捕获异常,可在 co_try / co_catch 语法中手动处理。

auto task() -> std::generator <int> {
    try {
        co_yield 1;
        throw std::runtime_error("boom");
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << '\n';
        co_yield -1;
    }
}

四、协程调度器(Scheduler)

为避免协程过多导致资源竞争,可实现一个轻量级调度器,统一管理协程队列。

class Scheduler {
public:
    void schedule(std::coroutine_handle<> h) { tasks.emplace_back(h); }
    void run() {
        while (!tasks.empty()) {
            auto h = tasks.front();
            tasks.pop_front();
            h.resume();
        }
    }
private:
    std::deque<std::coroutine_handle<>> tasks;
};

五、常见坑与解决方案

原因 解决方案
协程泄漏 coroutine_handle 未被 destroy 确保 co_return 后手动 coro.destroy() 或使用 RAII 包装
多线程安全 std::generator 非线程安全 每个线程使用独立协程实例,或使用 std::atomic/锁保护
堆栈溢出 递归协程深度过大 将递归改写为循环或使用分层协程

六、总结

C++20 协程为异步编程带来了更直观、更接近同步代码的写法。通过与 Boost.Asio 等库结合,可快速构建高性能网络服务。掌握 promise_type、await_suspend、await_resume 三个核心方法,是实现自定义协程的关键。

学习建议

  1. 从最小的 co_yield generator 开始。
  2. 逐步加入 I/O 事件循环。
  3. 关注异常传播与资源回收。

祝你在协程之路上越走越顺利!

发表评论