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/catch 或 unhandled_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()
。
流程:
async_read_some()
不会直接返回结果,而是注册一个事件回调。这个回调里,Boost.Asio 调用
handle.resume()
。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
表达式替代了回调机制,编译器会帮你:
在挂起前注册回调。
当异步完成时,恢复协程并调用
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 一次性处理 |