Boost.Asio 基础
本节不急着讲串口、TCP、UDP,而是把
io_context、run()、post()、work_guard、buffer、std::bind、对象生命周期这些基础讲清楚。
这些东西如果没搞懂,后面串口和网络程序会出现“为什么回调不执行”“为什么程序直接退出”“为什么段错误”等问题。
示例 1:io_context 没有任务时,run() 立刻返回
程序目标
验证一个非常重要的现象:如果 io_context 里没有任何未完成任务,io.run() 会立刻返回。
完整代码
#include <boost/asio.hpp>
#include <print>
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
// io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
boost::asio::io_context io;
std::println("main:准备调用 io.run()");
// 启动事件循环,前面注册的异步任务会在这里被调度执行。
std::size_t count = io.run();
std::println("main:io.run() 返回,执行了 {} 个回调", count);
return 0;
}运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。
运行输出与时间顺序
这个程序不会等待,立刻输出:
main:准备调用 io.run()
main:io.run() 返回,执行了 0 个回调本示例需要注意的点
io.run() 不是永远阻塞。它阻塞的前提是:
io_context 里还有未完成的异步任务,或者还有 work_guard 保持它不退出。如果什么任务都没有,它会马上返回。
io.run() 返回值
io.run() 返回值表示执行了多少个 handler。
本例中没有任何任务,所以返回值是:
0示例 2:普通 main() 里用 post() 投递任务
程序目标
boost::asio::post() 可以把一个普通函数投递到 io_context 里,让它以后由 io.run() 执行。
完整代码
#include <boost/asio.hpp>
#include <functional>
#include <print>
#include <string>
void print_msg(const std::string& msg)
{
std::println("执行任务:{}", msg);
}
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
// io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
boost::asio::io_context io;
std::println("main:投递任务 A");
// post 只是把任务放进队列,真正执行要等 io.run()。
boost::asio::post(io, std::bind(print_msg, std::string("A")));
std::println("main:投递任务 B");
boost::asio::post(io, std::bind(print_msg, std::string("B")));
std::println("main:准备调用 io.run()");
// 启动事件循环,前面注册的异步任务会在这里被调度执行。
std::size_t count = io.run();
std::println("main:io.run() 返回,执行了 {} 个任务", count);
return 0;
}运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。
运行输出与时间顺序
程序立刻输出:
main:投递任务 A
main:投递任务 B
main:准备调用 io.run()
执行任务:A
执行任务:B
main:io.run() 返回,执行了 2 个任务本示例需要注意的点
post() 不是立刻执行函数,而是把函数放进 io_context 的任务队列。
真正执行发生在:
io.run();post() 函数说明
boost::asio::post(io, handler);参数:
io:任务投递到哪个io_context;handler:将来要执行的函数对象。
返回值:通常不用关心。
std::bind 说明
std::bind(print_msg, std::string("A"))表示生成一个“无参数函数对象”。以后执行它时,相当于执行:
print_msg("A");示例 3:work_guard 让 io.run() 等待未来任务
程序目标
首先要理解 io.run() 一个非常重要的规则:
io.run()不是一个永远运行的死循环。
当 io_context 发现:
现在没有 handler 可以执行
并且
以后也没有已经登记的异步工作需要等待那么:
io.run();就会直接返回。
例如:
boost::asio::io_context io;
io.run();这里什么任务都没有,所以:
进入 run()
↓
发现没有任何工作
↓
立即返回这对于机器人通信线程就可能产生一个问题。
假设程序启动时先创建 Asio 线程:
通信线程
↓
io.run()但是串口、定时器或者其他任务要等一会儿才从别的线程提交。
那么可能发生:
Asio 线程启动
↓
io.run()
↓
此时任务队列为空
↓
run() 认为已经没事可做
↓
直接返回
↓
Asio 通信线程结束
过了 2 秒
↓
其他线程才 post() 一个任务
↓
已经没有线程在 run() 了
↓
这个任务没人执行work_guard 就是为了解决这种情况。
可以先把它理解成:
给
io_context人为登记一份“还有工作没有完成”的占位工作。
只要这个 guard 还有效:
io_context:
虽然现在任务队列是空的,
但还有一份 outstanding work,
所以我不能认为工作已经全部结束。于是:
io.run();不会因为“暂时没任务”而返回,而是继续阻塞等待以后可能到来的任务。
如果没有 work_guard
先看这个过程:
boost::asio::io_context io;
io.run();此时:
任务数量 = 0
未完成异步操作 = 0
work_guard = 0所以:
io.run()
↓
没有任何工作
↓
直接返回可以把 run() 粗略理解成:
while (还有工作需要处理)
{
等待事件
执行 handler
}
return;注意这里的:
还有工作不只是指:
当前任务队列里已经有 handler也包括:
已经启动但还没有完成的异步操作例如:
timer.async_wait(...);虽然 timer 还要 2 秒才到期,但 Asio 已经知道:
这里还有一个异步操作没完成所以这种情况下:
io.run();本来就不会立刻返回。
这也是为什么用 timer.async_wait() 来演示 work_guard 并不好:
timer 自己已经能够让
io.run()保持运行,反而看不出 guard 的作用。
work_guard 到底做了什么
创建:
auto guard = boost::asio::make_work_guard(io);以后,可以粗略理解成:
io_context 内部的“未完成工作数量”
额外 +1注意,这里的 +1 只是帮助理解,并不代表内部真的就是一个简单整数。
可以想象成:
创建 io_context
未完成工作:
0创建 guard:
make_work_guard(io)
未完成工作:
0 + 一份 guard 占位工作于是即使现在:
没有 timer
没有 socket
没有 post 的 handler
没有任何真正任务io.run() 看到 guard 还持有 outstanding work,也不会退出。
它会:
没有任务
↓
但是 guard 还在
↓
不能返回
↓
阻塞等待新任务注意:
work_guard不会让io.run()疯狂空转。
它不是:
while (true)
{
}没有任务时,io.run() 仍然可以阻塞等待,不会因为 guard 就白白占满一个 CPU 核心。
完整代码
这个例子真正模拟:
Asio 通信线程已经启动
但当前没有任何任务
2 秒以后另一个线程才提交任务#include <boost/asio.hpp>
#include <chrono>
#include <functional>
#include <print>
#include <thread>
using namespace std::chrono_literals;
using WorkGuard =
boost::asio::executor_work_guard<
boost::asio::io_context::executor_type>;
void delayed_post(
boost::asio::io_context* io,
WorkGuard* guard)
{
std::println("工作线程:等待 2 秒再提交任务");
std::this_thread::sleep_for(2s);
std::println("工作线程:向 io_context 提交任务");
boost::asio::post(
*io,
[] {
std::println(
"Asio handler:收到后来提交的任务");
});
std::println("工作线程:释放 work_guard");
guard->reset();
}
int main()
{
boost::asio::io_context io;
WorkGuard guard =
boost::asio::make_work_guard(io);
std::thread worker(
std::bind(
delayed_post,
&io,
&guard));
std::println("main:开始 io.run()");
std::size_t count = io.run();
std::println(
"main:io.run() 返回,执行了 {} 个 handler",
count);
worker.join();
return 0;
}运行结果大致为:
main:开始 io.run()
工作线程:等待 2 秒再提交任务
工作线程:向 io_context 提交任务
工作线程:释放 work_guard
Asio handler:收到后来提交的任务
main:io.run() 返回,执行了 1 个 handler具体打印先后可能因为线程调度略有变化,但核心流程不变。
整个执行过程
程序首先创建:
boost::asio::io_context io;此时:
io_context
↓
没有任何任务然后:
WorkGuard guard =
boost::asio::make_work_guard(io);相当于告诉 io_context:
虽然现在可能没有任务,
但先不要认为整个事件循环已经结束,
以后还可能有人给你提交工作。接着启动:
std::thread worker(...);这个线程会先:
std::this_thread::sleep_for(2s);所以刚开始的时候,io_context 里面其实真的没有任何 handler 可以执行。
此时 main 执行:
io.run();如果没有 guard,逻辑会变成:
io.run()
↓
当前没有任何工作
↓
立即返回但现在有 guard:
io.run()
↓
当前没有 handler
↓
但是 work_guard 仍然有效
↓
说明仍然存在 outstanding work
↓
不能退出
↓
继续阻塞等待于是 main 就停在:
io.run();里面等待。
2 秒以后发生什么
worker 睡眠结束:
std::this_thread::sleep_for(2s);然后执行:
boost::asio::post(
*io,
[] {
std::println(
"Asio handler:收到后来提交的任务");
});post() 相当于:
向
io_context投递一个以后要执行的 handler。
于是原本正在:
阻塞等待的 io.run() 发现:
有新任务来了然后执行这个 handler。
guard.reset() 到底做什么
接下来:
guard->reset();它并不是:
停止 io_context也不是:
取消所有任务更不是:
让 run() 立刻强制返回它真正表达的是:
我之前登记的那份“占位工作”现在结束了,你以后不用再为了我强行保持
run()存活。
可以粗略理解成:
创建 guard
outstanding work:
+1然后:
guard.reset()
outstanding work:
-1于是 io_context 又恢复正常退出规则:
如果还有真正的任务
→ 继续处理
如果还有未完成异步操作
→ 继续等待
如果什么都没有了
→ run() 可以返回所以:
guard.reset();不是:
命令 run() 退出而是:
取消“禁止 run() 因没工作而退出”这个限制这一点非常重要。
为什么 post 以后再 reset
本例中:
boost::asio::post(*io, handler);
guard->reset();顺序可以理解成:
先把真正的工作提交进去
↓
然后取消 guard 的占位工作这时 io_context 中仍然还有:
刚刚 post 的 handler所以即使 guard 被 reset:
还有真正工作
↓
run() 继续
↓
执行 handler
↓
handler 执行结束
↓
此时已经没有其他工作
↓
run() 返回完整过程:
创建 io_context
│
├─ 当前没有任务
│
↓
创建 work_guard
│
├─ 人为保持 outstanding work
│
↓
main:io.run()
│
├─ 当前没 handler
│
├─ 但 guard 还在
│
└─ 所以阻塞等待,不退出
│
│
│ worker 睡眠 2 秒
│
│ ↓
│ post(handler)
│ ↓
│ guard.reset()
│
↓
io.run() 收到 handler
│
↓
执行 handler
│
↓
handler 完成
│
├─ guard 已 reset
├─ 没有其他任务
├─ 没有未完成异步操作
│
↓
io.run() 返回这就是 work_guard 的完整作用。
如果一直不 reset() 会怎样
假设:
WorkGuard guard =
boost::asio::make_work_guard(io);
io.run();又永远不:
guard.reset();那么即使所有真正任务都已经执行完:
handler 全执行完了
timer 全结束了
socket 也没任务了io.run() 仍然会认为:
还有一份 outstanding work所以:
不会正常返回它会继续等待以后可能到来的任务。
这正是后台 Asio 线程经常需要的效果。
reset() 和 io.stop() 不一样
这两个很容易混。
guard.reset() 的意思是:
允许
io.run()在所有真正工作完成以后自然退出。
也就是:
reset()
↓
不再人为保持 io_context 存活
↓
还有任务就继续执行
↓
任务全部完成后 run() 自然返回而:
io.stop();更像:
要求这个
io_context尽快停止运行。
所以简单理解:
guard.reset()
→ 我不再拦着你退出
→ 剩余工作正常处理
→ 没工作以后自然退出而:
io.stop()
→ 主动要求事件循环停止因此如果希望:
尽量优雅地把已经提交的任务处理完再退出通常更关注:
guard.reset();如果希望主动让事件循环停止,则可以使用:
io.stop();机器人程序中为什么经常需要 work_guard
例如你的程序可能是:
main
│
├─ 创建 io_context
│
├─ 创建串口驱动
│
├─ 创建 Asio 通信线程
│
└─ ROS2 开始运行Asio 通信线程:
io.run();而真正的异步任务可能是之后才出现:
ROS2 callback
↓
要求发送串口命令
↓
post 到 io_context
或者
设备稍后连接
↓
启动 async_read
或者
以后动态创建 timer如果 Asio 线程刚启动时:
暂时没有任何 outstanding work那么没有 guard:
Asio 线程
↓
io.run()
↓
没任务
↓
直接返回
↓
通信线程死掉使用 guard:
Asio 线程
↓
io.run()
↓
暂时没任务
↓
guard 还在
↓
继续等待
↓
以后任务来了
↓
正常处理因此可以把 work_guard 理解成:
给长期运行的 Asio 事件循环办理了一张“先别下班”的凭证。
make_work_guard() 和 reset() 怎么记
创建:
auto guard =
boost::asio::make_work_guard(io);可以理解成:
告诉 io_context:
“我这里还有未来工作,
即使眼前没任务,你也先别让 run() 下班。”释放:
guard.reset();可以理解成:
告诉 io_context:
“我这边以后不会再依赖你继续保持运行了,
如果其他工作也都结束了,你可以让 run() 返回。”所以它们不是:
make_work_guard()
→ 创建一个实际任务
reset()
→ 删除一个实际任务而是:
make_work_guard()
→ 持有一份 outstanding work
reset()
→ 放弃这份 outstanding work本示例最重要的结论
没有 work_guard:
io.run()
↓
暂时没有工作
↓
直接返回有 work_guard:
io.run()
↓
暂时没有工作
↓
但 guard 仍持有 outstanding work
↓
继续阻塞等待未来任务释放 guard:
guard.reset()
↓
不再人为维持 io_context 存活
↓
如果真正工作也全部结束
↓
io.run() 返回所以一句话记住:
work_guard不是一个真正要执行的任务,而是一份“还有工作存在”的占位声明,用来防止io.run()在暂时无任务时提前返回。
示例 4:buffer 的基本使用
程序目标
Boost.Asio 读写数据时经常写:
boost::asio::buffer(data)本例先看它的基本含义。
完整代码
#include <boost/asio.hpp>
#include <array>
#include <print>
#include <string>
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
std::string msg = "hello asio";
auto buf1 = boost::asio::buffer(msg);
// std::array 是固定长度数组,长度在编译期就确定。
std::array<char, 128> data;
auto buf2 = boost::asio::buffer(data);
std::println("msg.size() = {}", msg.size());
std::println("buffer(msg).size() = {}", buf1.size());
std::println("array size = {}", data.size());
std::println("buffer(data).size() = {}", buf2.size());
return 0;
}运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。
运行输出
msg.size() = 10
buffer(msg).size() = 10
array size = 128
buffer(data).size() = 128本示例需要注意的点
boost::asio::buffer() 通常不会复制数据,它只是生成一个“指向已有内存的 buffer 视图”。
所以异步读写时特别注意:
buffer 指向的原始数据必须活到异步回调完成。错误写法示意:
void send()
{
std::string msg = "hello";
boost::asio::async_write(socket_, boost::asio::buffer(msg), handler);
}这个 msg 在 send() 返回后就销毁了,但异步写可能还没完成。
正确思路:
把 msg 做成类成员变量
或者用 shared_ptr 管理
或者使用写队列保存待发送数据boost::asio::buffer() 可以接收哪些类型
boost::asio::buffer(data) 的作用可以简单理解为:
把一段连续内存包装成 Asio 可以使用的 buffer 描述对象。
它本身不会复制数据,主要记录的是:
内存起始地址
+
内存长度所以最常见的要求是:
data最好是一段连续存储的内存。
常见可直接使用的类型包括:
char data[128];
std::array<char, 128> data;
std::array<std::uint8_t, 128> data;
std::vector<char> data;
std::vector<std::uint8_t> data;
std::string data;例如:
std::array<std::uint8_t, 256> data;
auto buffer = boost::asio::buffer(data);或者:
//创建一个名为 data 的 std::vector,里面有 256 个 std::uint8_t 元素。
std::vector<std::uint8_t> data(256);
auto buffer = boost::asio::buffer(data);字符串也可以:
std::string message = "hello";
auto buffer = boost::asio::buffer(message);还可以直接指定:
指针 + 长度例如:
std::uint8_t data[256];
auto buffer =
boost::asio::buffer(data, 100);表示只使用:
data 开始位置后的前 100 字节也可以:
std::uint8_t* data = ...;
std::size_t size = ...;
auto buffer =
boost::asio::buffer(data, size);对于串口、网络等字节流通信,通常优先使用:
std::uint8_t
unsigned char
char
std::byte例如:
std::array<std::uint8_t, 256> rx_buffer;这是很常见的接收缓冲区写法。
像下面这些容器不能简单看成一整块连续内存:
std::list
std::deque因此一般不能像:
std::vector
std::array
std::string那样直接作为一整块连续 buffer 使用。
还要注意 const。
例如:
const std::string message = "hello";那么:
boost::asio::buffer(message);得到的是只读 buffer,适合:
发送数据但不能作为接收缓冲区,因为接收操作需要修改底层内存。
可以简单理解成:
非 const 数据
→ 可以生成可写 buffer
const 数据
→ 只能生成只读 buffer最后还有一个非常重要的点:
boost::asio::buffer(data)不拥有data的内存。
例如:
std::string data = "hello";
auto buffer = boost::asio::buffer(data);这里 buffer 只是指向 data 的内存。
所以异步操作:
boost::asio::async_write(
socket,
boost::asio::buffer(data),
handler);完成之前,必须保证:
data 仍然存在
并且底层内存没有失效因此可以把:
boost::asio::buffer(data)简单记成:
把一段连续内存包装成“地址 + 长度”的 Asio buffer,并不会复制或拥有这段数据。
示例 5:类里封装 io_context 工作线程
程序目标
工程里经常希望 Asio 在单独线程运行。
本例写一个最小的 IoThread 类:
构造时创建
work_guard;start()开线程执行io.run();stop()停止线程;用
post()投递任务验证效果。
完整代码
#include <boost/asio.hpp>
#include <functional>
#include <print>
#include <memory>
#include <string>
#include <thread>
class IoThread
{
public:
IoThread()
: guard_(boost::asio::make_work_guard(io_))
{
}
~IoThread()
{
stop();
}
void start()
{
thread_ = std::thread(std::bind(&IoThread::run, this));
}
void stop()
{
if (!stopped_)
{
stopped_ = true;
guard_.reset();
io_.stop();
if (thread_.joinable())
{
thread_.join();
}
}
}
boost::asio::io_context& io()
{
return io_;
}
private:
void run()
{
std::println("IoThread:io.run() 开始");
// 启动事件循环,前面注册的异步任务会在这里被调度执行。
io_.run();
std::println("IoThread:io.run() 返回");
}
private:
// io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
boost::asio::io_context io_;
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> guard_;
std::thread thread_;
bool stopped_ = false;
};
void print_task(const std::string& msg)
{
std::println("执行任务:{}", msg);
}
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
IoThread io_thread;
io_thread.start();
// post 只是把任务放进队列,真正执行要等 io.run()。
boost::asio::post(io_thread.io(), std::bind(print_task, std::string("A")));
boost::asio::post(io_thread.io(), std::bind(print_task, std::string("B")));
std::this_thread::sleep_for(std::chrono::seconds(1));
std::println("main:准备停止 IoThread");
io_thread.stop();
std::println("main:结束");
return 0;
}运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。
运行输出与时间顺序
程序启动后,工作线程开始运行:
IoThread:io.run() 开始
执行任务:A
执行任务:B约 1 秒后主线程停止它:
main:准备停止 IoThread
IoThread:io.run() 返回
main:结束完整输出类似:
IoThread:io.run() 开始
执行任务:A
执行任务:B
main:准备停止 IoThread
IoThread:io.run() 返回
main:结束本示例需要注意的点
这个类就是以后封装串口、TCP、UDP 通信线程的雏形。
但注意:
io_.stop();会让 io_context 尽快停止,未完成的异步任务可能不会正常完成。
工程里更细致的做法是:
先 cancel socket / timer / serial_port
再 reset work_guard
最后等待 run() 返回本节总结
没有任务时,
io.run()立刻返回。post()可以把普通函数投递给io_context执行。work_guard可以防止io.run()因为没任务而退出。std::bind可以绑定普通函数,也可以绑定成员函数。boost::asio::buffer()通常只是内存视图,不负责延长数据生命周期。异步工程里最重要的是对象生命周期和 buffer 生命周期。

