C++20中的协程:从基本概念到实战应用

在 C++20 中,协程(coroutines)作为一种强大的异步编程工具正式被标准化。相比传统的线程或回调,协程在语义清晰、性能开销小、代码可读性高方面具有明显优势。本文将系统梳理协程的核心概念、实现细节,并给出一个完整的网络请求示例,帮助读者快速上手。

1. 协程的基本语法

协程函数使用 co_awaitco_yieldco_return 关键字实现:

co_return;   // 结束协程,返回值可用
co_yield x;  // 暂停并返回一个值,后续可继续
co_await expr; // 暂停等待 expr 的结果

协程函数必须返回 std::experimental::coroutine_handle 或者相关的协程返回类型。C++20 标准库提供了 std::generator(实验性)来简化生成器的实现。

2. 协程的执行模型

协程的执行模型分为两部分:

阶段 说明
生成 在调用协程函数时,编译器生成一个 悬挂对象,即 std::coroutine_handle<>。此时并未真正执行函数体。
恢复 当外部调用 handle.resume() 或者使用 co_yieldco_await 触发时,协程恢复执行,直至遇到下一个暂停点或结束。

协程的暂停点(co_awaitco_yieldco_return)会保存当前执行状态(局部变量、指令指针等),便于后续恢复。

3. 关键概念拆解

  1. Promise
    协程返回类型中会包含一个 promise 对象,负责定义协程的行为(如异常处理、返回值类型等)。Promise 的生命周期与协程句柄绑定,协程结束后自动销毁。

  2. Awaitable
    co_await 后面可以跟任意 awaitable 对象。编译器会在后台调用 await_ready()await_suspend()await_resume() 三个成员函数来决定协程是否立即返回、挂起或获取结果。

  3. Awaiter
    一个 awaitable 对象会提供 awaiter,负责实际的挂起/恢复逻辑。标准库中常见的 awaiter 如 std::suspend_alwaysstd::suspend_never

4. 常见协程返回类型

  • `std::generator `(实验性):用于生成器,支持 `co_yield`。
  • `std::future `:与 `std::async` 相似,支持 `co_await`。
  • 自定义 Promise:可实现更复杂的协程行为,如异步 IO、任务调度器等。

5. 实战示例:异步 HTTP GET 请求

下面给出一个使用 Boost.Asio + C++20 协程的异步 HTTP GET 示例,演示如何在协程中等待网络 IO。

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

namespace asio = boost::asio;
namespace beast = boost::beast;
namespace http = beast::http;

// 简化的 Awaitable:等待异步操作完成
template<class AsyncOperation>
struct awaitable
{
    AsyncOperation op;
    asio::yield_context yield;
    std::error_code ec;

    awaitable(AsyncOperation&& op, asio::yield_context yield)
        : op(std::move(op)), yield(yield) {}

    bool await_ready() const noexcept { return false; }
    void await_suspend(std::coroutine_handle<> h) { op(yield); }
    void await_resume() const noexcept { if (ec) throw std::system_error(ec); }
};

struct http_get
{
    std::string host, target;
    unsigned short port;
    asio::io_context& ioc;

    http_get(std::string host, std::string target, unsigned short port,
             asio::io_context& ioc)
        : host(std::move(host)), target(std::move(target)), port(port), ioc(ioc) {}

    std::future<std::string> operator()()
    {
        struct promise_type {
            std::promise<std::string> prom;
            http_get* self;

            auto get_return_object() { return std::future<std::string>(prom.get_future()); }
            std::suspend_never initial_suspend() { return {}; }
            std::suspend_never final_suspend() noexcept { return {}; }
            void return_value(std::string&& val) { prom.set_value(std::move(val)); }
            void unhandled_exception() { prom.set_exception(std::current_exception()); }
        };

        return co_spawn(ioc, [this]() -> std::string {
            beast::tcp_stream stream(ioc);
            beast::error_code ec;
            auto const results = asio::ip::tcp::resolver(ioc).resolve(host, std::to_string(port));
            stream.connect(results, ec);
            if (ec) throw std::system_error(ec);

            http::request<http::string_body> req{http::verb::get, target, 11};
            req.set(http::field::host, host);
            req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);

            http::write(stream, req, ec);
            if (ec) throw std::system_error(ec);

            beast::flat_buffer buffer;
            http::response<http::dynamic_body> res;
            http::read(stream, buffer, res, ec);
            if (ec) throw std::system_error(ec);

            return beast::buffers_to_string(res.body().data());
        });
    }
};

int main()
{
    asio::io_context ioc;
    auto fut = http_get("example.com", "/", 80, ioc)();
    ioc.run();
    std::cout << fut.get() << std::endl;
}

代码要点:

  • co_spawn 用于在协程里发起异步操作。
  • awaitable 封装了 asio::yield_context,让我们在协程内部使用 co_await 等待 IO 完成。
  • promise_type 负责把协程结果返回给 std::future,让调用者可以像同步代码一样获取结果。

6. 协程与线程池的融合

在高并发场景下,可以把协程与线程池结合使用,利用事件循环调度协程,避免频繁的线程切换。Boost.Asio、cppcoro、cppcoro-future 等库提供了成熟的协程调度器。实现思路:

  1. 将协程包装成 std::futurecppcoro::task
  2. 通过 io_context 或自定义调度器,将协程挂起/恢复交由线程池中的工作线程处理。
  3. 只在 IO 或 CPU 密集型任务时切换协程,保持低开销。

7. 性能注意事项

  • 避免不必要的堆分配:协程的 Promise 对象在堆上分配,尽量使用返回值优化(RVO)。
  • 精确控制挂起点:每一次 co_await 都会创建一个 awaiter,若频繁调用可能导致大量对象生命周期管理。
  • 使用 suspend_always/suspend_never:在不需要挂起的场景手动使用 suspend_never,减少调度开销。

8. 结语

C++20 的协程为异步编程提供了接近同步的语义,极大降低了编写高并发代码的门槛。掌握其基础概念后,可进一步探索协程的高级用法:协程生成器、协程管道、错误传播、资源管理等。通过结合现代网络库(如 Boost.Asio、cppcoro 等),你可以构建高性能、易维护的异步系统,满足当今复杂业务的需求。祝你在协程的海洋里畅游无阻!

发表评论