掌握C++20协程的实战技巧

C++20在标准库中引入了协程(coroutine)这一强大的异步编程工具,为编写高性能、低延迟的异步代码提供了更直观、更低级别的控制。本文将从协程的基本概念开始,结合实际案例,系统阐述协程的核心机制、实现细节以及常见的使用场景。

一、协程的基本概念

协程是一种轻量级的“挂起/恢复”函数,能够在执行过程中随时挂起,等到需要时再恢复执行。它与传统的线程或进程相比,拥有更小的栈空间、快速的切换以及更可读的代码结构。

1.1 协程的生命周期

  1. 创建:协程对象(如 std::coroutine_handle<>)被生成并绑定到实际的协程函数。此时协程函数的入口点被设置,但尚未执行。
  2. 挂起:通过 co_awaitco_yieldco_return,协程可以将控制权交回调用者,等待下一次恢复。
  3. 恢复:调用者通过 handle.resume() 将协程恢复到下一个挂起点。
  4. 结束:协程执行到 co_return 或抛出异常后,协程资源被销毁。

1.2 awaitableawaiter

  • awaitable:协程中可等待的对象,提供 await_readyawait_suspendawait_resume 三个成员函数。标准库提供了 std::futurestd::async 等已实现的 awaitable。
  • awaiter:由 awaitableoperator co_await 返回,负责挂起/恢复流程。

二、协程的实现细节

2.1 协程句柄与堆栈

C++ 协程通过 std::coroutine_handle<> 对象来管理协程的状态。协程的堆栈通常分为两部分:

  • 框架堆栈:由编译器生成,用于存放局部变量、返回地址等。
  • 协程堆栈:在 operator new 时动态分配,包含协程的执行上下文。

2.2 协程框架(State Machine)

编译器将协程转换为一个状态机。每个 co_awaitco_yield 等挂起点对应一个状态,调度器根据当前状态决定是否需要挂起或恢复。例如:

task <int> async_add(int a, int b) {
    co_await std::suspend_always{}; // 第1个挂起点
    int result = a + b;
    co_return result; // 终止
}

编译器会生成类似以下结构:

struct async_add_state {
    int a, b;
    int result;
    int state; // 0: 未开始, 1: 等待, 2: 完成
};

2.3 对象生命周期管理

由于协程堆栈可能跨越多个协程调用,必须小心资源释放。C++20 提供 std::suspend_alwaysstd::suspend_never,以及 co_return 的析构逻辑,帮助开发者控制资源清理。

三、实战案例:异步文件读取

以下代码演示了如何使用协程实现一个简单的异步文件读取器。示例基于 POSIX 事件轮询(epoll),但核心思想可迁移到任何 I/O 多路复用框架。

#include <coroutine>
#include <iostream>
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <vector>
#include <cstring>

struct async_read {
    struct promise_type {
        std::coroutine_handle<> continuation;
        int fd;
        char* buffer;
        size_t size;
        ssize_t result;

        async_read get_return_object() {
            return { std::coroutine_handle <promise_type>::from_promise(*this) };
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept {
            return {};
        }
        void return_value(ssize_t val) { result = val; }
        void unhandled_exception() { std::terminate(); }
    };

    std::coroutine_handle <promise_type> handle;
    async_read(std::coroutine_handle <promise_type> h) : handle(h) {}
    ~async_read() { if (handle) handle.destroy(); }

    // 启动协程
    void start() {
        handle.resume();
    }

    // 等待结果
    ssize_t get() { return handle.promise().result; }
};

struct await_readable {
    int fd;
    std::coroutine_handle<> continuation;

    bool await_ready() const noexcept { return false; }
    void await_suspend(std::coroutine_handle<> h) {
        continuation = h;
        // 注册到 epoll
        int epfd = ::epoll_create1(0);
        struct epoll_event ev{ .events = EPOLLIN, .data.fd = fd };
        ::epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
        // 等待就绪
        ::epoll_wait(epfd, &ev, 1, -1);
        ::close(epfd);
        continuation.resume();
    }
    void await_resume() const noexcept {}
};

async_read read_file_async(int fd, char* buffer, size_t size) {
    await_readable readable{ fd };
    co_await readable;
    ssize_t n = ::read(fd, buffer, size);
    co_return n;
}

int main() {
    int fd = ::open("test.txt", O_RDONLY);
    if (fd < 0) { perror("open"); return 1; }

    char buf[1024];
    auto reader = read_file_async(fd, buf, sizeof(buf));
    reader.start(); // 启动协程
    ssize_t n = reader.get(); // 获取结果
    std::cout << "Read " << n << " bytes: " << std::string(buf, n) << '\n';
    ::close(fd);
    return 0;
}

3.1 代码说明

  1. async_read:封装协程的 promise 和 handle,提供 start()get() 接口。
  2. await_readable:实现 awaitable,在 await_suspend 中使用 epoll 监视文件描述符可读事件。协程挂起,等到事件发生后再恢复。
  3. read_file_async:协程函数,首先挂起等待文件可读,然后调用 read 并返回读取字节数。

该示例展示了协程与传统回调、事件循环相比的优点:代码更线性、易于维护,并且协程切换的开销极小。

四、协程常见陷阱

  1. 无限挂起:若协程未正确 co_returnco_yield,调用 resume() 将导致死循环。始终确保有退出路径。
  2. 资源泄露:协程对象持有资源(如文件描述符、内存),若协程异常退出,需确保 promise_type::unhandled_exception 或自定义析构释放。
  3. 与多线程混用:协程本身是单线程的,若在多线程中调用 resume(),需使用同步机制避免竞争。

五、总结

C++20 的协程为异步编程提供了强大的工具,既能保持代码的同步式可读性,又能获得类似事件循环的性能优势。通过理解协程的生命周期、实现细节以及常见陷阱,开发者可以在实际项目中灵活运用协程,实现更高效、更易维护的异步代码。祝你编码愉快!

发表评论