利用C++20协程实现高效异步网络请求

在现代 C++ 开发中,异步编程已经成为处理 I/O 密集型任务的主流方式。传统的回调、线程池或者基于事件循环的模型虽然各有优点,但在语义清晰、错误处理以及资源管理方面往往显得繁琐。C++20 通过引入协程(coroutine)这一语言特性,提供了更直观的方式来书写异步代码。本文将从协程的基本概念出发,演示如何在 C++20 中使用协程实现一个简易的异步网络请求框架,并讨论其性能优势和使用注意事项。

1. 协程基础

协程是可挂起的函数,可以在执行过程中暂停并保存其状态,待未来某个时刻再恢复。C++20 中协程的核心关键字有 co_awaitco_yieldco_return,以及与之配合使用的 std::coroutine_handle。协程函数的返回类型不是普通的 intvoid,而是一个具备 promise_type 的自定义类型。

1.1 协程函数声明

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

    struct promise_type {
        Task get_return_object() { return {handle_type::from_promise(*this)}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::rethrow_exception(std::current_exception()); }
    };

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

Task async_operation();   // 这是一个协程函数

协程函数 async_operation 在被调用时并不会立即执行,而是返回一个 Task 对象,内部保存了协程句柄。只有当 coro.resume() 被显式调用时,协程才会开始执行。

1.2 co_await 的工作原理

co_await 用来等待一个可 Awaitable 的对象。可 Awaitable 的对象需要满足以下接口:

auto await_ready() -> bool;
auto await_suspend(std::coroutine_handle<>) -> void or bool;
auto await_resume() -> T;
  • await_ready():如果立即可用,返回 true,协程不暂停。
  • await_suspend():协程暂停,接收当前协程句柄,决定如何恢复。若返回 true,协程将挂起,需由外部恢复;若返回 false,协程立即恢复。
  • await_resume():协程恢复后,返回结果。

2. 简易异步 I/O 适配器

C++20 标准库并未提供网络 I/O 的异步实现,常见做法是借助第三方库(如 Boost.Asio、libuv 等)或直接包装系统调用。下面以 POSIX epoll 为例,构造一个可 Awaitable 的异步读取器。

2.1 epoll 事件包装

struct EpollEvent {
    int fd;
    uint32_t events;
    int epoll_fd;

    EpollEvent(int f, uint32_t ev, int ef) : fd(f), events(ev), epoll_fd(ef) {}

    bool await_ready() { return false; }

    bool await_suspend(std::coroutine_handle<> h) {
        epoll_event ev{ .events = events, .data.ptr = static_cast<void*>(h.address()) };
        if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) {
            h.destroy(); // 发生错误,直接销毁协程
            return false;
        }
        return true; // 挂起
    }

    int await_resume() {
        epoll_event ev;
        int nfds = epoll_wait(epoll_fd, &ev, 1, -1);
        // epoll_wait 返回后,协程自动恢复
        return nfds > 0 ? ev.events : -1;
    }
};

2.2 异步读取器

Task async_read(int fd, std::vector <char>& buffer, int epoll_fd) {
    EpollEvent ev{fd, EPOLLIN, epoll_fd};
    co_await ev; // 等待可读事件

    ssize_t n = read(fd, buffer.data(), buffer.size());
    if (n < 0) throw std::system_error(errno, std::generic_category(), "read");
    co_return;
}

3. 用协程实现 HTTP GET

接下来,演示一个简易的 HTTP GET 请求器。它使用 async_read 读取响应主体,并利用协程链式调用实现直观流程。

#include <iostream>
#include <vector>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

Task async_http_get(const std::string& host, const std::string& path, int epoll_fd) {
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) throw std::system_error(errno, std::generic_category(), "socket");

    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(80);
    inet_pton(AF_INET, host.c_str(), &addr.sin_addr);

    if (connect(sock, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0)
        throw std::system_error(errno, std::generic_category(), "connect");

    std::string request = "GET " + path + " HTTP/1.1\r\nHost: " + host + "\r\nConnection: close\r\n\r\n";
    write(sock, request.data(), request.size());

    std::vector <char> buffer(4096);
    while (true) {
        co_await EpollEvent{sock, EPOLLIN, epoll_fd};
        ssize_t n = read(sock, buffer.data(), buffer.size());
        if (n <= 0) break;
        std::cout.write(buffer.data(), n);
    }

    close(sock);
    co_return;
}

4. 调度器与事件循环

协程本身只是一个“挂起/恢复”的机制,真正决定何时恢复的责任在于调度器。下面给出一个最小化的事件循环实现:

int main() {
    int epoll_fd = epoll_create1(0);
    if (epoll_fd < 0) return 1;

    // 启动协程
    auto task = async_http_get("93.184.216.34", "/", epoll_fd); // example.com

    // 事件循环
    while (true) {
        epoll_event ev;
        int n = epoll_wait(epoll_fd, &ev, 1, -1);
        if (n <= 0) continue;

        // 恢复挂起的协程
        auto handle = static_cast<std::coroutine_handle<>>(ev.data.ptr);
        handle.resume();

        if (!handle.done()) continue;
        handle.destroy(); // 任务完成,销毁句柄
        break;
    }

    close(epoll_fd);
    return 0;
}

5. 性能与优势

  • 语义清晰:协程使异步流程写法接近同步代码,减少回调地狱。
  • 低延迟:协程暂停时不产生线程上下文切换,只有在需要 I/O 完成时挂起/恢复。
  • 资源友好:协程本身占用的栈空间极小,适合高并发场景。

6. 注意事项

  1. 异常传播:协程内部抛出的异常会被 promise_type::unhandled_exception() 捕获并转发,需在调用点使用 try-catch 处理。
  2. 事件注册/注销:在 await_suspendawait_resume 之间需要正确管理事件句柄,避免资源泄漏。
  3. 兼容性:C++20 协程仅在支持 -std=c++20 的编译器(如 GCC 10+、Clang 12+、MSVC 19.28+)中可用。
  4. 第三方库:许多成熟的异步 I/O 框架(如 asio::awaitable)已经对协程进行了封装,直接使用可避免自行实现细节。

7. 结语

通过本示例,读者可以看到 C++20 协程如何与系统级 I/O 结合,打造既简洁又高效的异步网络程序。未来随着标准库对异步 I/O 的进一步完善,协程将成为 C++ 生态中不可或缺的编程范式。祝你在异步编程的路上越走越远!

发表评论