《C++ 23:协程与同步编程的新前景》

协程(Coroutine)在 C++20 里正式加入语言核心,C++23 进一步完善了其标准库支持,尤其是对异步 I/O 的统一处理方式。本文将从协程的基本概念、实现原理以及在现代 C++ 项目中的典型使用场景入手,帮助读者快速上手并发挥协程的最大价值。

1. 协程概述

协程是一种轻量级的用户级线程,能够在运行时挂起并恢复执行,保持自己的局部状态。与传统的回调和事件循环相比,协程能让异步代码以同步写法显现,极大提升可读性与可维护性。

1.1 关键语言特性

  • co_awaitco_yieldco_return:三大协程关键词,分别用于挂起、产生值、返回结果。
  • std::future / std::promise 替代:协程不再需要显式的 Promise/Future 组合,返回值为 std::future 或自定义 awaitable 对象即可。
  • 协程句柄 std::coroutine_handle:管理协程生命周期,可主动销毁或恢复。

2. 实现原理

协程编译时会将函数体拆分成若干基本块,在每个挂起点生成一个 resume 函数,恢复时会跳转回该点。

  • 悬挂点co_awaitco_yieldco_return 会在编译时生成对应的 suspend/resume 逻辑。
  • 状态机:协程内部状态由一个 promise_type 对象维护,包含协程局部变量和挂起/恢复状态。
  • 堆分配:协程对象的状态通常放在堆上,避免栈空间碎片,且可随时扩展。

3. C++23 的改进

  1. std::generator:提供统一的生成器类型,简化 co_yield 的使用。
  2. std::ranges::subrange:与协程返回的范围配合,支持按需遍历。
  3. 异步 I/O 集成std::experimental::netawaitable 接口与协程配合使用,支持 POSIX 事件循环。
  4. 同步文件 I/Ostd::filesystem::file 提供 awaitable 接口,直接在协程里读取文件。

4. 典型使用案例

4.1 网络服务器

#include <iostream>
#include <netinet/in.h>
#include <unistd.h>
#include <experimental/coroutine>

using namespace std::experimental::literals;

struct AwaitableAccept {
    int sock;
    int operator()() const { return accept(sock, nullptr, nullptr); }
    bool await_ready() const noexcept { return false; }
    void await_suspend(std::coroutine_handle<> h) const noexcept {
        // 简化示例:直接挂起
        h.resume();
    }
    int await_resume() const noexcept { return operator()(); }
};

struct EchoServer {
    int listen_fd;

    auto operator()() -> std::experimental::coroutine_handle<> {
        while (true) {
            int client = co_await AwaitableAccept{listen_fd};
            // 简化处理:读取并写回
            char buf[512];
            int n = read(client, buf, sizeof buf);
            write(client, buf, n);
            close(client);
        }
    }
};

int main() {
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    sockaddr_in addr{AF_INET, htons(8080), INADDR_ANY};
    bind(fd, (sockaddr*)&addr, sizeof addr);
    listen(fd, 128);

    EchoServer{fd}(); // 启动协程
    // 事件循环(简化)
    std::this_thread::sleep_for(10s);
    close(fd);
}

4.2 并行任务调度

#include <vector>
#include <thread>
#include <future>
#include <experimental/coroutine>

struct Task {
    int id;
    std::string name;
};

struct AwaitableTask {
    Task t;
    bool await_ready() const noexcept { return false; }
    void await_suspend(std::coroutine_handle<> h) const noexcept {
        std::thread([=]{
            std::cout << "Running " << t.name << " (id=" << t.id << ")\n";
            std::this_thread::sleep_for(std::chrono::seconds(1));
            h.resume();
        }).detach();
    }
    void await_resume() const noexcept {}
};

auto worker(Task t) -> std::experimental::coroutine_handle<> {
    co_await AwaitableTask{t};
}

int main() {
    std::vector <Task> tasks = {{1, "A"}, {2, "B"}, {3, "C"}};
    for (auto &t : tasks) worker(t)(); // 启动所有协程
    std::this_thread::sleep_for(std::chrono::seconds(3));
}

5. 性能与调试

  • 栈占用:协程本身不占用栈,状态存放在堆上,避免栈溢出。
  • 调试难度:断点设置时需关注协程挂起点,IDE 需支持协程调试。
  • 异常传播promise_type::final_suspend 负责把异常抛给 await_resume

6. 结语

C++23 的协程与同步 I/O 生态,为开发者提供了高效、易读的异步编程模型。掌握 co_await 与标准库协程工具后,您可以将传统的回调地狱转化为清晰的协程链,极大提升大型项目的可维护性与性能。

祝你在 C++ 的协程世界里玩得开心!

发表评论