**C++ 23 中的协程:从基础到实践**

协程(coroutine)是 C++20 开始正式加入标准的一项强大功能,它为异步编程和生成器提供了一种简洁、可组合的语法。在 C++23 中,协程的实现进一步成熟,提供了更高效的状态机生成、可移植的调度器支持以及与标准库容器的深度集成。本文将从协程的概念入手,介绍其关键语法、典型使用场景以及在 C++23 中的新特性,并给出一段完整的演示代码,帮助你快速上手。


1. 协程的核心概念

术语 说明
协程函数 co_await, co_yield, co_return 的函数,返回类型为 std::experimental::generator, std::future, std::task
挂起点 co_await, co_yield, co_return 触发协程暂停并返回控制权
恢复点 当外部再次请求协程时,从挂起点继续执行
协程句柄 std::experimental::coroutine_handle,用于控制协程的生命周期(resume, destroy 等)
协程 Promise 每个协程都有一个 Promise 对象,用于携带返回值、异常以及协程状态

注意:协程在 C++23 中仍属于实验性特性,官方通过 <experimental/coroutine><coroutine> 实现;在实际项目中应使用对应的实现库(如 libcoro 或编译器自带实现)以获得稳定性。


2. 基本语法

#include <coroutine>
#include <iostream>
#include <vector>

struct Task {
    struct promise_type {
        Task get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
};

Task hello() {
    std::cout << "Hello, ";
    co_await std::suspend_always{};
    std::cout << "world!\n";
}
  • promise_type 定义了协程的行为:initial_suspendfinal_suspend 决定协程启动和结束时是否挂起。
  • co_await 用于挂起并等待异步结果;co_yield 用于生成器返回值;co_return 用于结束并返回值。

3. C++23 新特性

新特性 说明
std::generator 直接支持生成器语法,无需手动实现 Promise
std::task 轻量级异步任务,兼容 std::futurestd::shared_future
协程调度器 std::experimental::schedulers 提供基于事件循环的调度器实现
co_await 的异构支持 可以对任何实现 await_transform 的类型进行挂起
协程的复制 通过 std::experimental::coroutine_handle::move 实现协程句柄的移动语义

这些改进使得协程的使用更加自然,减少模板魔法,并为高并发编程提供了更好的性能。


4. 示例:协程 + 线程池 + HTTP 请求

下面的代码演示如何使用 C++23 的协程与线程池实现一个简单的异步 HTTP 客户端。我们使用 asio(Boost.Asio 或 standalone Asio)来处理网络 IO,并用协程包装异步操作。

#include <asio.hpp>
#include <iostream>
#include <coroutine>
#include <thread>
#include <vector>

using asio::ip::tcp;

// 简单的协程包装器
struct Awaitable {
    asio::ip::tcp::socket& socket;
    std::size_t size;
    char* data;
    asio::error_code ec = asio::error::make_error_code(asio::error::operation_aborted);

    struct promise_type {
        Awaitable get_return_object() {
            return {nullptr, 0, nullptr};
        }
        std::suspend_never initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::rethrow_exception(std::current_exception()); }
    };

    Awaitable operator co_await() const noexcept {
        return *this;
    }

    bool await_ready() const noexcept { return false; }

    void await_suspend(std::coroutine_handle<> h) {
        socket.async_read_some(
            asio::buffer(data, size),
            [h](asio::error_code ec, std::size_t /*bytes*/) mutable {
                if (ec) {
                    std::cerr << "Read error: " << ec.message() << '\n';
                }
                h.resume();
            });
    }

    void await_resume() {}
};

// 异步 HTTP GET
asio::awaitable<std::string> async_get(asio::io_context& io, const std::string& host, const std::string& path) {
    auto executor = co_await asio::this_coro::executor;
    tcp::resolver resolver(executor);
    auto endpoints = co_await resolver.async_resolve(host, "http", asio::use_awaitable);

    tcp::socket socket(executor);
    co_await asio::async_connect(socket, endpoints, asio::use_awaitable);

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

    std::string response;
    char buf[1024];
    for (;;) {
        std::size_t n = co_await socket.async_read_some(asio::buffer(buf), asio::use_awaitable);
        if (n == 0) break;
        response.append(buf, n);
    }
    co_return response;
}

// 线程池
struct ThreadPool {
    asio::io_context io;
    std::vector<std::thread> workers;

    ThreadPool(std::size_t n = std::thread::hardware_concurrency()) {
        workers.reserve(n);
        for (std::size_t i = 0; i < n; ++i)
            workers.emplace_back([this] { io.run(); });
    }

    ~ThreadPool() { io.stop(); for (auto& t : workers) t.join(); }
};

int main() {
    ThreadPool pool;
    auto [response] = asio::co_spawn(pool.io, async_get(pool.io, "example.com", "/"), asio::detached);
    // 此时协程已在线程池中执行,主线程可继续其他工作
    std::cout << "Fetched " << response.size() << " bytes\n";
    return 0;
}

说明

  1. 线程池:使用 asio::io_contextrun() 在多个线程中调度协程。
  2. 异步 async_get:使用 asio::awaitable 作为返回类型,内部调用 async_* 操作并通过 co_await 让协程挂起。
  3. co_spawn:启动协程并将其绑定到 io_contextasio::detached 表示不等待结果(可改为 use_future 获取 std::future)。

5. 常见坑 & 性能优化

问题 解决方案
协程句柄泄漏 明确在 final_suspend 后调用 destroy(),或者使用 std::suspend_always 并在 return_object() 中返回句柄。
异常传播 promise_type::unhandled_exception 默认重新抛出,若想自定义可在此处记录日志。
堆栈开销 协程状态机在堆上分配,若频繁创建请使用 co_yield 生成器或 std::generator
线程安全 多线程访问共享数据时仍需同步;协程仅保证挂起点与恢复点的顺序。
调试难度 使用 -fsanitize=address -fsanitize=undefined-fdiagnostics-color=always 可以帮助定位问题。

6. 小结

  • C++23 的协程让异步代码写得更像同步代码,极大提升可读性与可维护性。
  • asio 等异步库配合使用,可实现高性能网络应用、游戏循环或 GUI 事件驱动。
  • 需要注意的是协程本身是轻量级的,但真正的 IO 仍由底层事件循环或线程池完成。

通过掌握协程的基本语法与 C++23 的新特性,你可以在自己的项目中快速引入高并发、低延迟的异步机制,从而提升整体性能与开发效率。祝你编码愉快!


发表评论