如何使用C++20的协程实现异步I/O?

C++20 通过协程(coroutine)机制为异步编程带来了革命性的简化。传统的异步 I/O 需要回调、状态机或多线程,易导致“回调地狱”,而协程允许我们以同步代码的写法实现异步流程。下面以 asio(Boost.Asio 或独立的 ASIO)为例,演示如何利用协程实现一个简单的异步 TCP 客户端。

1. 环境准备

  • 编译器支持 C++20(例如 GCC 11+,Clang 13+,MSVC 16.10+)
  • 安装 ASIO(如果你不想依赖 Boost,直接使用独立版)
# 安装独立版 ASIO(假设你在 Linux)
sudo apt-get install libasio-dev

2. 基本思路

  1. 异步操作包装
    ASIO 提供了 async_* 形式的接口,如 async_connect, async_read, async_write。这些函数接受一个可调用对象作为完成处理器(handler)。我们要把它们包装成返回 std::future 或更好,直接返回 `awaitable

    `。
  2. 协程入口
    使用 asio::co_spawn 创建一个协程。协程内部使用 co_await 等待异步操作完成,代码像同步流程一样直观。

  3. 错误处理
    协程内部的 co_await 可以捕获异常,使用 try-catch 结构处理错误。

3. 示例代码

以下代码展示了一个异步 TCP 客户端,它连接到服务器、发送请求并接收响应:

#include <asio.hpp>
#include <asio/steady_timer.hpp>
#include <asio/coroutine.hpp>
#include <iostream>
#include <string>

using asio::ip::tcp;
using asio::awaitable;
using asio::use_awaitable;
using namespace std::chrono_literals;

// 简单的 awaitable wrapper
awaitable <void> async_echo(tcp::socket& socket, const std::string& message)
{
    // 发送消息
    std::size_t sent = co_await asio::async_write(
        socket,
        asio::buffer(message),
        use_awaitable
    );

    // 接收回复
    std::vector <char> buf(1024);
    std::size_t recvd = co_await asio::async_read(
        socket,
        asio::buffer(buf),
        use_awaitable
    );

    std::string reply(buf.data(), recvd);
    std::cout << "Received: " << reply << '\n';
}

awaitable <void> client(const std::string& host, const std::string& port)
{
    try
    {
        // 获取 I/O 上下文
        asio::io_context io_ctx{1};

        // 创建协程的 socket
        tcp::socket socket{io_ctx};

        // 解析地址
        auto endpoints = co_await tcp::resolver{io_ctx}.async_resolve(host, port, use_awaitable);

        // 连接
        co_await asio::async_connect(socket, endpoints, use_awaitable);
        std::cout << "Connected to " << host << ':' << port << '\n';

        // 发送并接收
        co_await async_echo(socket, "Hello from C++20 coroutine!");

        // 关闭 socket
        socket.close();
    }
    catch (const std::exception& ex)
    {
        std::cerr << "Error: " << ex.what() << '\n';
    }
}

int main()
{
    asio::io_context io_ctx;
    // 创建协程并运行
    asio::co_spawn(io_ctx, client("127.0.0.1", "12345"), asio::detached);
    io_ctx.run();
    return 0;
}

代码说明

  • use_awaitable:将 ASIO 的异步接口转换为协程可 co_await 的形式。
  • `awaitable `:协程返回类型,表示没有返回值;若有返回值,可改为 `awaitable`。
  • co_await:等待异步操作完成,内部会挂起当前协程,直到操作完成。
  • asio::co_spawn:在指定的 io_context 上启动协程,asio::detached 表示不关心协程返回值。

4. 性能与优势

  • 简洁性:异步代码像同步一样书写,消除回调嵌套。
  • 可组合:协程间可以使用 co_await 进行组合,实现复杂的异步流程。
  • 资源友好:协程在挂起时几乎不占用栈空间,轻量级。
  • 错误传播:异常在协程内抛出,外层可统一捕获。

5. 进阶技巧

  1. 使用 steady_timer 实现超时

    auto timer = co_await asio::steady_timer::async_wait(use_awaitable);
  2. 多协程并发
    通过 asio::co_spawn 生成多个协程,每个协程处理不同连接,io_context 共享 I/O 资源。

  3. 自定义 awaitable
    为复杂的异步操作编写自己的 awaitable 类型,进一步提高可读性。

6. 常见坑

  • 使用错误的 use_awaitable:一定要在 asio::async_* 后面加 use_awaitable,否则返回的是回调方式。
  • 错误未捕获:协程内部若不捕获异常,异常会被 io_context 捕获并打印,但程序会继续运行,可能导致资源泄露。
  • 缺乏 io_context.run():所有协程必须在 io_context.run() 循环中执行,否则不会被调度。

7. 小结

C++20 协程让异步 I/O 代码既高效又易读。通过 asioawaitable 接口,开发者可以在不牺牲性能的前提下,写出类似同步的异步程序。随着标准库持续完善,未来我们将看到更多针对网络、文件和数据库的原生协程支持,极大提升 C++ 在异步领域的竞争力。

发表评论