Boost.Asio学习(6):Boost.Asio搭配c++协程

发布于:2025-08-05 ⋅ 阅读:(8) ⋅ 点赞:(0)

Boost.Asio 协程核心类与函数

Boost.Asio 在 C++20 协程模式下主要提供三大组件:

boost::asio::awaitable<T>

  • Boost 的协程任务类型,类似我们之前写的 Task<T>

  • 用于包装一个协程函数,让它可以 co_await

  • 定义方式:

    boost::asio::awaitable<void> my_coro();
    

    对比纯 C++20:

  • 纯 C++20:必须自己写 Task<T>promise_type

  • Boost:直接提供 awaitable<T>,不需要手写 promise。

boost::asio::co_spawn()

  • 启动协程的工具函数。

  • 将协程挂到指定 executor(如 io_context)中运行。

  • 调用方式:

    co_spawn(io_context, my_coro(), boost::asio::detached);
    
  • 参数

    • Executor:告诉在哪个执行器(线程)运行。

    • 协程:返回 awaitable<T> 的函数;调用时需要在此传递参数给协程

    • Completion Token:协程结束后的处理方式(如 detached 表示不关心结果)。

  • 对比纯 C++20:

    • 纯 C++20:必须自己手动 resume() 协程。

    • Boost:co_spawn 自动调度并 resume 协程。

Completion Token

选项 作用
boost::asio::detached 默认,表示不关心结果,协程结束后自动清理资源(最常用)
回调函数 当协程完成时调用,比如 [](std::exception_ptr e, T result){}
boost::asio::use_future 协程结果封装到 std::future,可 .get() 获取
boost::asio::redirect_error(token, ec) 错误重定向到外部 error_code,而不是抛异常

 1. detached(最常用)

co_spawn(io, echo_session(std::move(socket)), boost::asio::detached);
  • 不需要返回值,不捕获异常。

  • 内部逻辑:如果协程抛异常,默认调用 std::terminate

2. 回调方式

co_spawn(io,
    []() -> awaitable<int> {
        co_return 42;
    },
    [](std::exception_ptr e, int result) {
        if (e) {
            try { std::rethrow_exception(e); } catch (const std::exception& ex) {
                std::cerr << "Error: " << ex.what() << "\n";
            }
        } else {
            std::cout << "Result: " << result << "\n";
        }
    });
  • 如果协程返回 awaitable<int>,回调的第二个参数就是 int

  • 如果发生异常,e 会非空。

3. use_future

auto fut = co_spawn(io,
    []() -> awaitable<int> {
        co_return 99;
    },
    boost::asio::use_future);

std::cout << "Result: " << fut.get() << "\n";  // 阻塞等待结果
  • 适用于不想写回调,但又需要返回值的场景。

  • 注意:fut.get() 会阻塞线程。

4. redirect_error

boost::system::error_code ec;
co_spawn(io,
    []() -> awaitable<void> {
        throw std::runtime_error("Something went wrong");
        co_return;
    },
    boost::asio::redirect_error(boost::asio::detached, ec));

io.run();

if (ec) {
    std::cerr << "Caught error: " << ec.message() << "\n";
}

避免异常传播,改用 error_code。 

boost::asio::use_awaitable

  • 这是一个 Completion Token,让 Boost 异步函数返回 awaitable<T>

  • 用法:

    std::size_t n = co_await socket.async_read_some(buffer, use_awaitable);
    
  • 作用:

    • 告诉 Asio:这个异步操作不要用回调,而是返回一个 awaitable<T>

  • 对比纯 C++20:

    • 纯 C++20:没有 IO 框架,需要自己写 IO 轮询和挂起点。

    • Boost:帮你把异步 IO 封装成 co_await 友好形式。

功能 纯 C++20 协程 Boost.Asio 协程
任务类型 必须手写 Task<T>promise_type 提供 awaitable<T>
调度器 必须自己写线程池或事件循环 io_context + co_spawn() 自动调度
异步操作封装 手动写 awaiter,自己处理 epoll/kqueue use_awaitable 自动把 async_* 转成 awaitable
错误处理 自己写 try/catchunhandled_exception 提供 redirect_error
上手难度 高(必须懂 promise_type + handle) 低(直接用 awaitable

 例子学习

写一个最简单的asio回声服务器,接收一个连接然后回复;acceptor使用异步接收,收发都用异步的async,使用协程代替回调;会详细解释所有用到协程有关内容的地方

Echo Server(单连接版本)

#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <iostream>

using boost::asio::ip::tcp;
using namespace boost::asio;

awaitable<void> echo_session(tcp::socket socket) {
    try {
        char data[1024];
        for (;;) {
            // **异步读取**
            std::size_t n = co_await socket.async_read_some(buffer(data), use_awaitable);
            // ↑
            // co_await: 挂起协程,直到 async_read_some 完成。
            // async_read_some 返回 awaitable<size_t>,由 use_awaitable 指定。

             // ✅ 在这里直接处理数据

            // **异步写回**
            co_await async_write(socket, buffer(data, n), use_awaitable);
            // ↑
            // 这里同理,写操作也被挂起,完成后恢复。
        }
    } catch (std::exception& e) {
        std::cerr << "Session ended: " << e.what() << "\n";
    }
}

awaitable<void> listener(io_context& io, unsigned short port) {
    tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), port));

    for (;;) {
        tcp::socket socket = co_await acceptor.async_accept(use_awaitable);
        // ↑
        // acceptor.async_accept() 返回 awaitable<tcp::socket>
        // co_await 让协程挂起,直到新连接到达。
        
        std::cout << "New connection accepted\n";

        // 启动新的协程处理该连接
        co_spawn(io, echo_session(std::move(socket)), detached);
        // ↑
        // co_spawn: 创建一个协程任务并调度到 io_context。
        // detached: 不关心返回值,完成后自动销毁。
    }
}

int main() {
    try {
        io_context io;

        // 启动监听协程
        co_spawn(io, listener(io, 5555), detached);
        // ↑
        // listener 是协程函数,返回 awaitable<void>。
        // co_spawn 会创建 promise_type,管理协程的生命周期。

        std::cout << "Server running on port 5555...\n";
        io.run();  // 事件循环,驱动协程继续执行
    } catch (std::exception& e) {
        std::cerr << "Error: " << e.what() << "\n";
    }
}

co_await socket.async_read_some(buffer, use_awaitable)

  • async_read_some 是 Asio 异步 API。

  • Normally:你会传回调 handler(error_code, size_t)

  • 现在:use_awaitable 告诉它返回 awaitable<size_t>,而不是回调。

  • co_await

    • 编译器会生成 awaiter 对象(Boost 已实现)。

    • async_read_some 完成时,事件循环 resume 协程。

  • 对比纯 C++20

    • 纯 C++20 你必须自己写 awaiter 类(包含 await_ready / await_suspend / await_resume)。

    • Boost 已经封装了 IO 挂起逻辑。

执行过程简化图

main()
  co_spawn(listener)
    ↓
 listener()
   co_await acceptor.async_accept()
     [挂起,返回 io_context]
 io.run()
   [监听 socket → accept 完成]
   resume(listener)
 listener() -> co_spawn(echo_session)
 echo_session()
   co_await socket.async_read_some()
     [挂起,等数据]

关键点:co_await

co_await 是挂起点,等操作完成后自动 resume 协程,但这个“自动”其实涉及一整套机制,我们可以分成三个核心问题来理解:

当编译器看到:

std::size_t n = co_await socket.async_read_some(buffer, use_awaitable);

它会:

  • 生成状态机(类似 switch-case)。

  • 把当前协程的执行位置保存到协程帧(stackless coroutine)。

  • 调用 socket.async_read_some() 返回一个 awaitable<T> 对象。

  • 调用其 operator co_await(),获取一个 awaiter 对象。

  • 执行:

    • awaiter.await_ready() → 如果 true,直接继续执行协程。

    • 如果 false,调用 awaiter.await_suspend(coroutine_handle) 挂起协程,把 handle 交给 IO 事件循环。

  • 当事件完成后,调用 awaiter.await_resume() 恢复协程并返回结果。

协程为什么能“自动 resume”?

因为 事件完成时,IO 框架(Boost.Asio)会拿到协程的 coroutine_handle 并调用 resume()

流程:

  1. async_read_some() 不会直接返回结果,而是注册一个事件回调。

  2. 这个回调里,Boost.Asio 调用 handle.resume()

  3. resume() 让协程继续执行状态机,从挂起点之后继续跑。

换句话说,co_await 的核心是:

  • 挂起协程(保存状态)

  • 把 resume 权交给 IO 框架

为什么协程比回调优雅?

传统写法(容易写成回调地狱):

socket.async_read_some(buffer, [](error_code ec, std::size_t n) {
    // 这里继续 async_write
});

协程写法:

std::size_t n = co_await socket.async_read_some(buffer, use_awaitable);
co_await async_write(socket, buffer(data, n), use_awaitable);

co_await 不是阻塞,而是:

注册回调 + 挂起协程 + 当异步完成时 resume()
把“异步事件”变成了“顺序代码”。

如果要处理读写后的data内容,直接在co_await后面写即可 

操作 回调风格 协程风格
异步读后处理 写在 lambda 内部 直接写在 co_await 后一行
状态管理 通过捕获变量或 shared_ptr 共享状态 协程帧自动保存变量(data 会保留)
错误处理 每一层手动检查 ec try/catch 一次性处理

 处理差异2:

  • 回调风格 下,异步 API 把结果通过参数传给回调函数(常见:error_code ec, size_t length)。

  • 协程风格 下,同样的结果会作为 co_await 的返回值,不再依赖回调参数。

因为协程用 co_await 表达式替代了回调机制,编译器会帮你:

  1. 在挂起前注册回调。

  2. 当异步完成时,恢复协程并调用 await_resume(),把回调参数返回给 co_await 的左值。

例子对比

回调写法

socket.async_read_some(boost::asio::buffer(data),
    [](boost::system::error_code ec, std::size_t length) {
        if (!ec) {
            std::cout << "Read " << length << " bytes\n";
        }
    });
  • 结果传递方式:回调参数 length

  • 问题:必须写 lambda,逻辑碎片化。

协程写法

std::size_t length = co_await socket.async_read_some(boost::asio::buffer(data), use_awaitable);
std::cout << "Read " << length << " bytes\n";
  • 结果传递方式co_await 表达式直接返回 length

  • 原理

    • async_read_some()use_awaitable 模式下,不返回 void,而是返回 awaitable<size_t>

    • await_resume() 会把 length 作为返回值。

    • 编译器自动把回调参数封装在 promise 里,恢复协程时传回给你。

use_awaitable 是关键:

  • 它告诉 async_* 系列 API:不要用 handler 回调,而是用 awaitable

  • 内部逻辑:

    • 创建一个 promise_type

    • async_read_some 内部注册回调。

    • 当 IO 完成,调用 promise_type.set_result(length)

    • 最后 await_resume() 返回这个值。

协程的优势总结

回调风格 协程风格
参数通过回调函数传入 直接用 auto result = co_await ... 获取
需要捕获外部变量 协程帧自动保存局部变量
错误处理要在回调里处理 try/catch 一次性处理

网站公告

今日签到

点亮在社区的每一天
去签到