Table of contents
Open Table of contents
TL;DR
Asio 的异步操作只写一次实现,通过 async_result<CompletionToken, Signature> trait 自动适配回调、stackful 协程、stackless 协程、C++20 协程和 future。这是 Asio 最精妙的设计——一个 async_read 实现同时支持五种异步风格,新增风格只需特化一个 trait。
先看结果:同一个操作,五种写法
以「从 socket 异步读取数据」为例,同一个 async_read_some 支持所有写法:
1. 回调(Callback)
socket.async_read_some(asio::buffer(buf),
[](asio::error_code ec, std::size_t bytes) {
if (!ec) {
// 处理数据
}
});
最原始的形式。handler 是一个函数对象(lambda/函数指针/bind 表达式),操作完成时被调用。
2. Asio 自带的 Stackless 协程
struct session : asio::coroutine {
tcp::socket& socket_;
std::array<char, 1024> buf_;
void operator()(asio::error_code ec = {}, std::size_t n = 0) {
reenter(this) {
for (;;) {
yield socket_.async_read_some(asio::buffer(buf_), *this);
yield async_write(socket_, asio::buffer(buf_, n), *this);
}
}
}
};
使用预处理器宏(reenter、yield)实现,底层是 Duff’s Device(switch 语句技巧)。每个 yield 点记录行号,下次调用时跳转到上次暂停的位置。
不需要分配协程栈,状态存储在成员变量中。代价是语法怪异、调试困难。
3. Stackful 协程(boost::asio::spawn)
asio::spawn(io, [&](asio::yield_context yield) {
for (;;) {
auto n = socket.async_read_some(asio::buffer(buf), yield);
async_write(socket, asio::buffer(buf, n), yield);
}
});
基于 Boost.Context 纤程。每个协程有自己的执行栈(默认 64KB-1MB)。yield 作为完成令牌传入,挂起协程直到操作完成。
写起来像同步代码,可以在任意调用深度挂起。代价是每个协程都要分配栈内存。
错误处理:默认抛 system_error 异常。用 yield[ec] 捕获错误码:
asio::error_code ec;
auto n = socket.async_read_some(asio::buffer(buf), yield[ec]);
if (ec) { /* 处理错误 */ }
4. C++20 协程(co_await + use_awaitable)
asio::awaitable<void> session(tcp::socket socket) {
std::array<char, 1024> buf;
for (;;) {
auto n = co_await socket.async_read_some(
asio::buffer(buf), asio::use_awaitable);
co_await async_write(socket,
asio::buffer(buf, n), asio::use_awaitable);
}
}
// 启动协程
asio::co_spawn(io, session(std::move(socket)), asio::detached);
编译器生成状态机,不需要分配协程栈(只分配编译器计算的 frame 大小)。语法最自然。
错误处理:默认把 error_code 转换为 system_error 异常抛出。三种替代方式:
// 方式 1:as_tuple —— 返回 tuple<error_code, result>
auto [ec, n] = co_await socket.async_read_some(
asio::buffer(buf), asio::as_tuple(asio::use_awaitable));
// 方式 2:redirect_error —— 将错误码写入变量
asio::error_code ec;
auto n = co_await socket.async_read_some(
asio::buffer(buf), asio::redirect_error(asio::use_awaitable, ec));
// 方式 3:自定义 awaitable 的默认 token
using default_token = asio::as_tuple_t<asio::use_awaitable_t<>>;
using tcp_socket = default_token::as_default_on_t<asio::ip::tcp::socket>;
// 之后所有操作默认返回 tuple,不需要每次写 as_tuple
5. std::future(use_future)
std::future<std::size_t> fut =
socket.async_read_some(asio::buffer(buf), asio::use_future);
// 在其他地方等待结果
auto n = fut.get(); // 阻塞直到完成
用 std::future 桥接异步和同步世界。通常只在需要和非 Asio 代码集成时使用。
完成令牌(Completion Token)机制:一次实现,多种适配
核心问题
如果每种异步风格都要写一遍实现,Asio 的每个异步操作(async_read、async_write、async_connect…)都要维护五份代码。这不现实。
解决方案:async_result trait
async_result<CompletionToken, Signature> 是一个 trait,负责将完成令牌转换为实际的 handler,并决定发起函数的返回类型:
发起函数调用路径:
socket.async_read_some(buffer, token)
│
├─ token = lambda → async_result 直接用 lambda 作为 handler,返回 void
├─ token = use_awaitable → async_result 创建 awaitable 适配器,返回 awaitable<T>
├─ token = use_future → async_result 创建 promise/future 对,返回 future<T>
├─ token = yield_context → async_result 挂起当前协程,返回 T
└─ token = deferred → async_result 创建延迟执行器,返回可调用对象
具体来说,每种完成令牌通过特化 async_result 实现适配:
// 伪代码:async_result 的核心概念
template <typename CompletionToken, typename Signature>
struct async_result {
using handler_type = /* 从 token 推导出的 handler 类型 */;
using return_type = /* 发起函数的返回类型 */;
explicit async_result(handler_type& h);
return_type get(); // 返回发起函数的返回值
};
为什么这很重要
写一个新的异步操作时,只需要通过 async_initiate 描述操作逻辑:
template <typename CompletionToken>
auto async_my_operation(tcp::socket& socket, CompletionToken&& token) {
return asio::async_initiate<CompletionToken, void(asio::error_code)>(
[&socket](auto handler) {
// 操作逻辑:只写一次
// handler 的实际类型由 async_result 决定
},
token);
}
这个操作自动支持所有完成令牌——回调、协程、future、deferred——不需要额外代码。
deferred:惰性组合的利器
deferred(Boost 1.78+)是一种特殊的完成令牌,它不立即启动操作,而是返回一个可调用对象,调用时才启动:
// 创建一个延迟操作(不会立即执行)
auto op = socket.async_read_some(asio::buffer(buf), asio::deferred);
// 之后再决定如何执行
op(actual_handler); // 用回调
co_await std::move(op); // 或者 co_await
deferred 的价值在于操作组合——你可以先构建一个操作链,再一次性启动:
auto read_then_write = socket.async_read_some(asio::buffer(buf), asio::deferred)
| asio::deferred([&](asio::error_code ec, std::size_t n) {
return async_write(socket, asio::buffer(buf, n), asio::deferred);
});
co_await std::move(read_then_write);
相比 use_awaitable,deferred 的优势是不为延迟操作本身分配协程帧。
协程取消(Cancellation in Coroutines)
C++20 协程中的取消机制:
asio::awaitable<void> my_coro() {
// 查询当前取消状态
auto cs = co_await asio::this_coro::cancellation_state;
// 修改支持的取消类型
co_await asio::this_coro::reset_cancellation_state(
asio::enable_total_cancellation());
// 操作...
}
新创建的协程默认只支持 cancellation_type::terminal(最安全的取消级别)。
取消类型的三个级别:
| 级别 | 含义 | 安全性 |
|---|---|---|
terminal | 取消后 I/O 对象只能 close/destroy | 最安全 |
partial | 取消可能有部分副作用(报告已传输字节数) | 中等 |
total | 取消无可观察副作用 | 最灵活 |
各模型对比
| 维度 | 回调 | Stackless 协程 | Stackful 协程 | C++20 协程 | future |
|---|---|---|---|---|---|
| C++ 版本 | C++11 | C++03 | C++11 | C++20 | C++11 |
| 内存开销 | 最小 | 最小 | 每协程一个栈(64KB+) | 编译器计算的帧 | promise/future 对 |
| 挂起点 | 不适用 | 显式(yield 宏) | 任意调用深度 | 显式(co_await) | 不适用 |
| 错误处理 | error_code 参数 | error_code 参数 | 异常或 ec | 异常或 as_tuple | 异常 |
| 可读性 | 差(嵌套) | 差(宏) | 好 | 最好 | 中等 |
| 可组合性 | 差 | 中等 | 好 | 最好 | 差 |
| 调试难度 | 中等 | 困难 | 中等 | 中等 | 简单 |
推荐选择
- 新项目(C++20 可用):用
co_await+use_awaitable(或deferred) - 不能用 C++20:用
spawn+yield_context(stackful 协程) - 高连接数、内存敏感:回调或 stackless 协程
- 与同步代码集成:
use_future
版本时间线
| 版本 | 异步模型里程碑 |
|---|---|
| Boost 1.47 (2011) | signal_set |
| Boost 1.54 (2013) | stackful 协程(spawn/yield_context) |
| Boost 1.72 (2019) | C++20 协程支持(awaitable, use_awaitable, co_spawn) |
| Boost 1.78 (2021) | deferred 完成令牌 |
| Boost 1.80 (2022) | experimental::parallel_group |
| Boost 1.84+ (2024) | co_composed 改进 |