### 如何在 C++20 中使用协程实现异步编程

在 C++20 中,协程(coroutine)被正式引入语言层面,提供了一种自然、轻量的方式来实现异步流式计算。相较于传统的回调、线程或消息队列,协程可以让代码保持同步写法,却能够隐藏底层的异步等待细节。下面从基本概念、语法特性到完整示例,系统讲解如何在 C++20 中使用协程实现异步编程。


1. 基本概念

术语 说明
co_await 暂停协程执行,等待一个可等待对象完成
co_yield 生成一个值给调用者,并暂停协程
co_return 结束协程并返回最终值
`task
| 一种表示异步结果的类型,类似std::future`,但更轻量

协程的关键是 挂起(suspend)与 恢复(resume)。当协程遇到 co_await 时,它会将控制权交还给外部调度器,等待等待对象完成后再恢复执行。这样可以避免阻塞线程。


2. 语法要点

struct task {
    struct promise_type {
        task get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
};
  • promise_type:协程的“承诺”对象,负责管理协程生命周期。你可以在里面自定义 initial_suspendfinal_suspendreturn_value 等。
  • suspend_never:表示不挂起。通常 initial_suspend 需要挂起,final_suspend 可以根据需要决定是否挂起。

3. 示例:异步网络请求

下面的示例演示了一个基于 asio(Boost.Asio)的简单 HTTP GET 请求,并使用协程进行异步处理。为了简化,省略了错误处理。

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

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

struct async_task {
    struct promise_type;
    using handle_type = std::coroutine_handle <promise_type>;
    handle_type coro;

    async_task(handle_type h) : coro(h) {}
    ~async_task() { if (coro) coro.destroy(); }

    struct promise_type {
        async_task get_return_object() { return async_task{handle_type::from_promise(*this)}; }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
};

async_task fetch_https(const std::string& host, const std::string& path) {
    io_context ctx;
    ssl::context ctx_ssl{ssl::context::sslv23_client};

    tcp::resolver resolver(ctx);
    auto endpoints = co_await resolver.async_resolve(host, "https", use_awaitable);
    tcp::socket socket(ctx);

    co_await async_connect(socket, endpoints, use_awaitable);

    ssl::stream<tcp::socket> ssl_stream(std::move(socket), ctx_ssl);
    co_await ssl_stream.async_handshake(ssl::stream_base::client, use_awaitable);

    std::string request = "GET " + path + " HTTP/1.1\r\nHost: " + host + "\r\nConnection: close\r\n\r\n";
    co_await async_write(ssl_stream, buffer(request), use_awaitable);

    boost::asio::streambuf response;
    co_await async_read_until(ssl_stream, response, "\r\n\r\n", use_awaitable);

    std::istream resp_stream(&response);
    std::string status_line;
    std::getline(resp_stream, status_line);
    std::cout << "Response status: " << status_line << std::endl;

    // 继续读取 body
    co_await async_read(ssl_stream, response, transfer_all(), use_awaitable);
    std::cout << "Body:\n" << &response << std::endl;
}

int main() {
    io_context ctx;
    fetch_https("www.example.com", "/").coro.promise().coro.resume(); // 手动启动协程
    ctx.run();
}

说明

  1. use_awaitable 是 Boost.Asio 提供的用于协程的 awaitable 适配器。
  2. co_awaitasync_* 组合实现了非阻塞 I/O。
  3. async_task 封装了协程句柄,简化了协程的创建和销毁。

4. 协程与 std::future 的对比

特点 std::future co_await / 协程
线程开销 可能涉及线程 无线程,轻量挂起
错误传播 get() 会抛异常 通过 promise_type 统一处理
代码可读性 嵌套回调 直观同步写法
资源管理 shared_future/promise co_returnco_yield 控制

协程在 I/O 密集型任务、事件驱动模型中表现更佳,尤其与 ASIO、libuv 等事件循环配合使用时。


5. 常见陷阱

  1. 忘记挂起:若 initial_suspend 返回 suspend_never,协程会立即执行完毕,可能导致后续 co_await 无法正常挂起。
  2. 生命周期管理:协程对象若超出作用域导致句柄失效,需要使用 std::shared_ptrstd::async 等包装。
  3. 异常捕获:协程内部抛出的异常会传递给 promise_type::unhandled_exception,需自行决定是否抛出或记录。

6. 小结

C++20 协程为异步编程提供了语言级别的原语,让我们可以像写同步代码一样书写异步逻辑。结合现代 I/O 库(如 Boost.Asio、libuv 等),协程成为构建高性能网络服务器、客户端的核心技术之一。掌握 co_awaitco_yieldpromise_type 的细节,将帮助你在项目中写出更简洁、更易维护的异步代码。

发表评论