C++中的协程:从概念到实践

协程(Coroutines)是 C++20 引入的一项强大功能,它允许你在函数内部暂停和恢复执行,从而简化异步编程、生成器、状态机等场景。下面我们从概念、语法、实现原理、常见用法和性能考虑等角度,系统地剖析协程。


一、协程的基本概念

  1. 暂停与恢复
    协程不像线程那样完全隔离;它在同一个线程内运行,但能在指定点挂起(co_awaitco_yieldco_return)并在之后恢复。挂起点保留了局部变量状态,真正的上下文切换发生在编译器生成的状态机中。

  2. 协程函数
    在 C++20 中,协程函数的返回类型必须是支持 promise_type 的类型。最常见的有 `std::generator

    `(C++23)和自定义的 `task`。
  3. 协程句柄
    std::coroutine_handle 是对协程内部实现的可操作句柄,允许手动调度、检查完成状态等。


二、语法与实现细节

1. 关键字

关键字 用途
co_await 等待另一个协程或异步操作完成,暂停当前协程
co_yield 在生成器中产生一个值,暂停当前协程
co_return 结束协程并返回值(如 voidint 等)
co_awaitco_yield 需要配合 awaitable / generator 类型

2. Promise 类型

协程函数返回的类型必须具备内部的 promise_type,它定义了协程运行时的生命周期函数:

struct MyTask {
    struct promise_type {
        MyTask get_return_object() {
            return MyTask{ std::coroutine_handle <promise_type>::from_promise(*this) };
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_value(int v) { value = v; }
        void unhandled_exception() { std::terminate(); }
        int value;
    };
    std::coroutine_handle <promise_type> h;
};

3. 状态机生成

编译器会把协程函数编译成一个状态机,每个 co_await/co_yield 对应一个状态点。状态机内部维护 promise_type,并在挂起时保存局部变量。


三、常见使用场景

1. 异步 I/O

结合 std::experimental::filesystem::file、网络库(如 Boost.Asio 或 libuv):

task <void> async_read(std::string path) {
    auto buffer = std::make_shared<std::vector<char>>(1024);
    auto fd = co_await open_async(path, O_RDONLY);
    size_t n = co_await read_async(fd, buffer.get(), buffer->size());
    co_return;
}

2. 生成器

实现无限序列(斐波那契数列):

generator <int> fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        int tmp = a + b;
        a = b; b = tmp;
    }
}

3. 状态机

用协程实现网络协议解析:

task <void> parse_protocol(stream& s) {
    while (true) {
        auto header = co_await s.read_exact(sizeof(Header));
        if (!header) co_return; // 关闭连接
        auto body = co_await s.read_exact(header.length);
        process(body);
    }
}

四、性能与限制

项目 说明
栈占用 协程不需要单独线程栈,堆上存储局部变量,内存更小
上下文切换 由于是状态机,切换开销低于线程切换,但比单线程同步略高
异常传播 通过 promise_type::unhandled_exception,异常会在协程恢复点抛出
标准库支持 C++23 引入 std::generatorstd::task,更方便使用
兼容性 需要 C++20/23 编译器,部分老旧编译器尚不完全支持

五、实战案例:协程驱动的轻量级 HTTP 服务器

#include <iostream>
#include <netinet/in.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <coroutine>

struct task {
    struct promise_type { /* ... 省略实现 ... */ };
};

task handle_client(int fd) {
    char buf[1024];
    while (true) {
        ssize_t n = co_await async_read(fd, buf, sizeof(buf));
        if (n <= 0) break;
        // 简单响应
        std::string response = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, world";
        co_await async_write(fd, response.c_str(), response.size());
    }
    close(fd);
}

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    // 省略 bind / listen 代码
    int epoll_fd = epoll_create1(0);
    struct epoll_event ev{ .events = EPOLLIN, .data.fd = server_fd };
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev);

    while (true) {
        struct epoll_event events[10];
        int n = epoll_wait(epoll_fd, events, 10, -1);
        for (int i = 0; i < n; ++i) {
            if (events[i].data.fd == server_fd) {
                int client_fd = accept(server_fd, nullptr, nullptr);
                handle_client(client_fd); // 协程启动
            }
        }
    }
}

该服务器使用 epoll + 协程实现事件驱动,避免了线程池开销,能够处理成千上万并发连接。


六、学习路径建议

  1. 先掌握 C++20 基础(编译器特性、标准库)
  2. 了解协程的语义co_awaitco_yieldco_return
  3. 阅读官方实现libc++libstdc++ 中协程实现源码
  4. 动手实践:从生成器、任务调度到网络 I/O
  5. 关注性能:剖析协程状态机生成、内存占用、异常路径

七、结语

协程是 C++20/23 的重要里程碑,为高并发、低延迟的应用开发提供了更简洁的模型。掌握协程不仅能提升代码可读性,还能在性能上获得显著优势。未来随着标准库的完善,协程将在异步编程领域扮演更加核心的角色。祝你在 C++ 的协程世界里玩得开心!

发表评论