串口通信教学
串口是机器人里最常见的通信方式之一:上位机 ROS2 / Linux 程序通过 USB-TTL、USB-CAN 转串口、CH340、CP2102、FT232 等设备和 STM32、ESP32、下位机控制板通信。
本节依旧遵循顺序:
先写普通 main() 里的同步示例
再写普通 main() 里的异步示例
最后写类封装版本所有异步回调统一使用 std::bind。
Linux 下准备虚拟串口测试环境
如果你手里没有真实 STM32,可以用 socat 创建一对虚拟串口。
安装 socat
Ubuntu / Debian:
sudo apt install socatFedora:
sudo dnf install socat创建一对虚拟串口
新开一个终端,运行:
socat -d -d \
PTY,raw,echo=0,link=/tmp/ttyV0 \
PTY,raw,echo=0,link=/tmp/ttyV1你会看到类似输出:
2026/05/24 12:00:00 socat[12345] N PTY is /dev/pts/3
2026/05/24 12:00:00 socat[12345] N PTY is /dev/pts/4
2026/05/24 12:00:00 socat[12345] N starting data transfer loop with FDs [5,5] and [7,7]这表示:
/dev/pts/3 和 /dev/pts/4 是一对互通的虚拟串口
程序写 /dev/pts/3,另一个终端读 /dev/pts/4 就能看到
程序读 /dev/pts/3,另一个终端写 /dev/pts/4 就能发给程序
同时我也给了他们别名
/dev/pts/3 和 /tmp/ttyV0是同一个东西
/dev/pts/4 和 /tmp/ttyV1是同一个东西下面示例都假设程序使用 /dev/pts/3,另一个终端使用 /dev/pts/4。你要按自己终端实际输出修改。
示例 1:普通 main() 里同步打开串口并发送一行
程序目标
程序打开串口 /dev/pts/3,设置波特率 115200,然后发送:
hello serial另一个终端从 /dev/pts/4 读到这行数据。
完整代码
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <print>
#include <string>
int main(int argc, char* argv[])
{
// 程序从 main 函数开始执行,argc/argv 用来接收命令行参数。
std::string port_name = "/dev/pts/3";
if (argc >= 2)
{
port_name = argv[1];
}
// io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
boost::asio::io_context io;
// serial_port 表示串口设备,后续读写都通过它完成。
boost::asio::serial_port serial(io);
boost::system::error_code ec;
std::println("main:准备打开串口 {}", port_name);
serial.open(port_name, ec);
if (ec)
{
std::println("打开串口失败:{}", ec.message());
return 1;
}
serial.set_option(boost::asio::serial_port_base::baud_rate(115200));
serial.set_option(boost::asio::serial_port_base::character_size(8));
serial.set_option(boost::asio::serial_port_base::parity(
boost::asio::serial_port_base::parity::none));
serial.set_option(boost::asio::serial_port_base::stop_bits(
boost::asio::serial_port_base::stop_bits::one));
serial.set_option(boost::asio::serial_port_base::flow_control(
boost::asio::serial_port_base::flow_control::none));
std::string msg = "hello serial\n";
// 把缓冲区中的数据写入连接。
std::size_t n = boost::asio::write(serial, boost::asio::buffer(msg), ec);
if (ec)
{
std::println("发送失败:{}", ec.message());
return 1;
}
std::println("main:发送成功,字节数 = {}", n);
std::println("main:程序结束");
return 0;
}运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。
编译运行
终端 1:创建虚拟串口。
socat -d -d \
PTY,raw,echo=0,link=/tmp/ttyV0 \
PTY,raw,echo=0,link=/tmp/ttyV1终端 2:监听另一端。
cat /dev/pts/4终端 3:编译运行程序。
运行刚才写的那个cpp来发送数据。
运行输出与时间顺序
程序终端输出:
main:准备打开串口 /dev/pts/3
main:发送成功,字节数 = 13
main:程序结束
监听终端输出:
hello serial
这个示例基本没有明显延迟,发送完成后程序立刻结束。
本示例需要注意的点
这是同步写串口。boost::asio::write() 会阻塞,直到数据写完或发生错误。
关键函数说明
serial.open(port_name, ec)
作用:打开串口设备。
参数:
port_name例如 /dev/ttyUSB0、/dev/ttyACM0、/dev/pts/3。
ec用于接收错误信息。这样写不会抛异常,更适合教程和机器人工程。
serial.set_option(...)
作用:设置串口参数,例如波特率、数据位、校验位、停止位、流控。
boost::asio::write(serial, boost::asio::buffer(msg), ec)
作用:同步写数据。(阻塞式发数据)
返回值:实际写入的字节数。
示例 2:普通 main() 里同步读取串口数据
程序目标
程序打开串口 /dev/pts/3,设置波特率 115200,然后阻塞等待接收 11 个字节。
另一个终端向 /dev/pts/4 发送:
hello asio由于 echo 会自动在末尾添加换行符 \n,实际发送的是:
hello asio\n一共 11 个字节。
完整代码
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <array>
#include <print>
#include <string>
int main(int argc, char* argv[])
{
// 程序从 main 函数开始执行,argc/argv 用来接收命令行参数。
std::string port_name = "/dev/pts/3";
if (argc >= 2)
{
port_name = argv[1];
}
// io_context 是 Asio 的 I/O 上下文。
boost::asio::io_context io;
// serial_port 表示串口设备,后续读写都通过它完成。
boost::asio::serial_port serial(io);
boost::system::error_code ec;
std::println("main:准备打开串口 {}", port_name);
serial.open(port_name, ec);
if (ec)
{
std::println("打开串口失败:{}", ec.message());
return 1;
}
serial.set_option(
boost::asio::serial_port_base::baud_rate(115200));
serial.set_option(
boost::asio::serial_port_base::character_size(8));
serial.set_option(
boost::asio::serial_port_base::parity(
boost::asio::serial_port_base::parity::none));
serial.set_option(
boost::asio::serial_port_base::stop_bits(
boost::asio::serial_port_base::stop_bits::one));
serial.set_option(
boost::asio::serial_port_base::flow_control(
boost::asio::serial_port_base::flow_control::none));
// 创建一个 11 字节的接收缓冲区。
std::array<char, 11> buffer{};
std::println("main:等待接收 11 个字节");
// 同步读取串口数据。
// 会一直阻塞,直到整个 buffer 被填满或者发生错误。
std::size_t n =
boost::asio::read(
serial,
boost::asio::buffer(buffer),
ec);
if (ec)
{
std::println("读取失败:{}", ec.message());
return 1;
}
// 把实际读取到的 n 个字节转换成字符串。
std::string msg(buffer.data(), n);
std::println("main:读取成功,字节数 = {}", n);
std::println("main:收到的数据 = {}", msg);
std::println("main:程序结束");
return 0;
}运行结果:见下方“运行输出与时间顺序”;如果示例涉及串口、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。
编译运行
终端 1:创建虚拟串口。
socat -d -d \
PTY,raw,echo=0,link=/tmp/ttyV0 \
PTY,raw,echo=0,link=/tmp/ttyV1假设得到:
/dev/pts/3
/dev/pts/4终端 2:编译运行程序。
编译运行程序后,
程序此时输出:
main:准备打开串口 /dev/pts/3
main:等待接收 11 个字节
然后程序会阻塞等待串口数据。
终端 3:向另一端发送数据。
echo "hello asio" > /dev/pts/4

运行输出与时间顺序
程序启动后输出:
main:准备打开串口 /dev/pts/3
main:等待接收 11 个字节随后程序执行:
boost::asio::read(
serial,
boost::asio::buffer(buffer),
ec);因为:
std::array<char, 11> buffer{};buffer 的大小是 11 字节,所以程序会一直阻塞,直到接收到 11 个字节。
当另一个终端执行:
echo "hello asio" > /dev/pts/4实际发送的数据是:
hello asio\n正好 11 个字节。
程序随后继续输出:
main:读取成功,字节数 = 11
main:收到的数据 = hello asio
main:程序结束这里看起来会多换一行,是因为收到的数据末尾本身包含:
\n这个示例的等待时间取决于你什么时候向串口发送数据。
本示例需要注意的点
这是同步读串口。
boost::asio::read() 会阻塞当前线程,直到指定的 buffer 被填满或者发生错误。
本例中:
std::array<char, 11> buffer{};所以:
boost::asio::read(
serial,
boost::asio::buffer(buffer),
ec);表示:
一直等待并读取数据
↓
累计收到 11 个字节
↓
read() 返回如果只发送:
hello由于还没有达到 11 个字节,程序仍然会继续阻塞等待剩余数据。
因为这里使用的是同步读取,所以不需要:
io.run();如果实际收到的数据远远超过 11 个字节,会不会把 array 撑爆?
不会。
例如本例的接收缓冲区只有:
std::array<char, 11> buffer{};所以:
boost::asio::buffer(buffer)告诉 Asio 的信息相当于:
这块接收内存最多只有 11 字节因此,即使串口实际上已经收到了 100 个字节:
0 1 2 3 4 5 6 7 8 9 10 11 12 ... 99这次:
boost::asio::read(
serial,
boost::asio::buffer(buffer),
ec);也只会向 buffer 中写入 11 个字节。
可以理解成:
串口接收数据:
┌───────────────────────────────────────────────┐
│ 前 11 字节 │ 剩余数据 │
└───────────────────────────────────────────────┘
↓
写入 buffer
std::array<char, 11>
┌───┬───┬───┬─────┬────┐
│ 0 │ 1 │ 2 │ ... │ 10 │
└───┴───┴───┴─────┴────┘多出来的数据不会继续往这个 array 后面写,因此不会把 array 写越界。
剩余的数据仍然留在操作系统的串口接收缓冲区中,等待程序下一次读取。
例如串口一次收到了:
ABCDEFGHIJKLMNOPQRSTUVWXYZ一共 26 个字节。
第一次读取 11 个字节:
ABCDEFGHIJK剩下:
LMNOPQRSTUVWXYZ还没有被程序取走。
下一次读取时,会从 array 的哪里开始写?
如果下一次还是这样:
boost::asio::read(
serial,
boost::asio::buffer(buffer),
ec);那么会再次从:
buffer[0]开始写。
也就是说,同一个 buffer 会被重新使用。
例如第一次读取后:
buffer:
┌───┬───┬───┬───┬────────┐
│ A │ B │ C │ D │ ... │
└───┴───┴───┴───┴────────┘
↑
buffer[0]第二次再次读取:
boost::asio::read(
serial,
boost::asio::buffer(buffer),
ec);新收到的数据仍然从:
buffer[0]开始写,因此会覆盖上一次保存的数据。
可以理解成:
第一次读取:
buffer
↓
ABCDEFGHIJK第二次读取:
buffer
↓
LMNOPQRSTUV第二次的数据会把第一次的数据覆盖掉。
所以如果上一次的数据后面还需要使用,就应该在下一次读取之前:
复制出去或者:
解析处理掉不能指望同一个 array 自动帮你保存每一次读取的数据。
如果剩余数据不够填满整个 array 呢?
例如第一次已经读取:
ABCDEFGHIJK串口还剩:
LMNOPQRSTUVWXYZ第二次读取 11 个字节:
LMNOPQRSTUV这时候串口只剩:
WXYZ只有 4 个字节。
如果第三次再次执行:
boost::asio::read(
serial,
boost::asio::buffer(buffer),
ec);因为 buffer 需要 11 个字节,所以过程会变成:
先读取已有的:
WXYZ
↓
只有 4 字节
↓
还差 7 字节
↓
继续阻塞等待
↓
以后又收到 7 字节
↓
累计达到 11 字节
↓
read() 返回所以:
boost::asio::read()关注的是“把你提供的整个 buffer 填满”,而不是“一次串口发送对应一次读取”。
串口本质上是连续的字节流,并不会自动帮程序保存“这一条消息”和“下一条消息”的边界。
关键函数说明
serial.open(port_name, ec)
作用:打开串口设备。
参数:
port_name例如:
/dev/ttyUSB0
/dev/ttyACM0
/dev/pts/3ec用于接收错误信息。
如果打开失败:
if (ec)
{
std::println("打开串口失败:{}", ec.message());
return 1;
}serial.set_option(...)
作用:设置串口参数,例如波特率、数据位、校验位、停止位和流控。
本示例设置:
波特率:115200
数据位:8
校验位:无
停止位:1
流控:无也就是常见的:
115200 8N1std::array<char, 11> buffer{}
作用:创建一个 11 字节大小的接收缓冲区。
std::array<char, 11> buffer{};可以简单理解成:
准备一块 11 字节的连续内存
用来存放这一次读取到的串口数据它的容量固定为 11 字节。
如果串口中的数据超过 11 字节:
本次最多读取 11 字节
↓
多出来的数据不会写越界
↓
剩余数据等待下一次读取如果下一次继续使用同一个:
buffer那么新数据会再次从:
buffer[0]开始写,覆盖上一次的数据。
boost::asio::buffer(buffer)
作用:把 buffer 对应的连续内存包装成 Asio 可以使用的 buffer。
boost::asio::buffer(buffer)可以简单理解成告诉 Asio:
这块内存从哪里开始
+
这块内存有多大本例中相当于告诉 Asio:
起始地址:buffer.data()
可用空间:11 字节因此 Asio 不会向这块内存写入超过 11 字节的数据。
它本身不会复制数据,也不会拥有这块内存。
boost::asio::read(serial, boost::asio::buffer(buffer), ec)
作用:同步读取串口数据。
std::size_t n =
boost::asio::read(
serial,
boost::asio::buffer(buffer),
ec);其中:
serial表示从哪个串口读取。
boost::asio::buffer(buffer)表示:
接收到的数据放到哪里
+
需要读取多少数据ec用于接收错误信息。
返回值:
std::size_t n表示实际读取到的字节数。
本例中正常读取完成后:
n = 11所以可以简单理解为:
boost::asio::read()会同步阻塞读取数据,直到指定的整个 buffer 被填满或者发生错误。
std::string msg(buffer.data(), n)
作用:把接收到的字节转换成字符串,方便输出。
buffer.data()表示 buffer 的起始地址。
n表示实际读取到的字节数。
因此:
std::string msg(buffer.data(), n);可以理解成:
从 buffer 的开头
取出前 n 个字节
↓
构造一个 std::string一句话记住:
boost::asio::read()只会向你提供的 buffer 范围内写数据;如果串口里还有更多数据,多出来的部分会留待下一次读取,而下一次使用同一个 buffer 时,会重新从buffer[0]开始写并覆盖旧数据。
some()相关的读取函数(不常用)
read() / write() 和 read_some() / write_some() 的区别
Boost.Asio 的同步读写接口可以分成两组:
boost::asio::read()
boost::asio::write()和:
serial.read_some()
serial.write_some()它们最大的区别是:
read()/write()会尽量把整个指定 buffer 处理完;read_some()/write_some()只进行一次读写操作,实际处理多少就返回多少。
boost::asio::read()
例如:
std::array<char, 11> buffer{};
std::size_t n =
boost::asio::read(
serial,
boost::asio::buffer(buffer),
ec);这里 buffer 大小是 11 字节。
boost::asio::read() 默认会一直读取,直到:
累计读取到 11 字节或者:
发生错误例如串口的数据并不是一次性全部到达:
第一次来了 3 字节
第二次来了 5 字节
第三次来了 3 字节read() 可以理解成:
第一次读取 3 字节
↓
还差 8 字节
↓
继续等
第二次读取 5 字节
↓
还差 3 字节
↓
继续等
第三次读取 3 字节
↓
累计 11 字节
↓
read() 返回所以:
boost::asio::read(...)可以简单理解成:
我要把这个 buffer 读满再返回。
serial.read_some()
例如:
std::array<char, 100> buffer{};
std::size_t n =
serial.read_some(
boost::asio::buffer(buffer),
ec);虽然 buffer 可以装 100 字节,但是:
read_some()并不要求一定读取 100 字节。
如果当前只读到了 8 字节,就可能直接:
n = 8然后返回。
例如:
buffer 最大容量:100 字节
当前串口可以读取:8 字节
↓
read_some()
↓
读取 8 字节
↓
立即返回所以:
read_some()可以简单理解成:
有多少先给我读一些,不要求把整个 buffer 填满。
boost::asio::write()
例如:
std::string msg = "hello asio";
std::size_t n =
boost::asio::write(
serial,
boost::asio::buffer(msg),
ec);假设:
msg = 10 字节boost::asio::write() 会一直尝试写,直到:
整个 10 字节都写完或者:
发生错误例如底层第一次只能写 4 字节:
准备发送 10 字节
↓
第一次写 4 字节
↓
还剩 6 字节
↓
继续写
↓
又写 6 字节
↓
全部完成
↓
write() 返回 n = 10所以:
boost::asio::write(...)可以简单理解成:
我要把整个 buffer 都写出去再返回。
serial.write_some()
例如:
std::string msg = "hello asio";
std::size_t n =
serial.write_some(
boost::asio::buffer(msg),
ec);即使:
msg 一共有 10 字节这一次调用也不保证一定全部写完。
例如可能:
准备发送:10 字节
↓
write_some()
↓
实际写入:4 字节
↓
返回 n = 4剩下的:
6 字节需要程序后续继续处理。
所以:
write_some()可以简单理解成:
这一次能写多少就先写多少,然后告诉我实际写了多少。
四个 API 对比
| API | 类型 | 是否要求整个 buffer 完成 |
|---|---|---|
boost::asio::read() | 同步读取 | 是,默认一直读到整个 buffer 被填满或发生错误 |
serial.read_some() | 同步读取 | 否,读取到一些数据就可以返回 |
boost::asio::write() | 同步写入 | 是,默认一直写到整个 buffer 写完或发生错误 |
serial.write_some() | 同步写入 | 否,这次实际写多少就返回多少 |
可以直接记成:
read()
→ 读满
read_some()
→ 读一些write()
→ 写完
write_some()
→ 写一些为什么 read() / write() 不是 serial 的成员函数?
你会发现:
serial.read_some(...)
serial.write_some(...)属于:
serial对象自己的成员函数。
但是:
boost::asio::read(...)
boost::asio::write(...)却是:
boost::asio命名空间中的普通函数。
这是因为:
read_some()
write_some()属于底层的单次 I/O 操作。
而:
boost::asio::read()
boost::asio::write()属于更高层的组合操作。
例如:
boost::asio::write(
serial,
boost::asio::buffer(msg),
ec);可以粗略理解成内部不断进行:
write_some()
↓
没写完
↓
继续 write_some()
↓
没写完
↓
继续
↓
整个 buffer 写完同理:
boost::asio::read(...)可以粗略理解成:
read_some()
↓
buffer 还没满
↓
继续 read_some()
↓
还没满
↓
继续
↓
整个 buffer 填满因此可以理解为:
read_some() / write_some()
↓
底层单次操作
read() / write()
↓
建立在底层操作之上的组合操作而且 boost::asio::read() 和 boost::asio::write() 不只可以用于串口,还可以用于其他符合 Asio 读写接口要求的对象,例如 socket。
所以 Asio 没有给每一种设备都分别设计:
serial.read()
serial.write()
socket.read()
socket.write()而是统一提供:
boost::asio::read(...)
boost::asio::write(...)使用时怎么选择?
如果你的需求是:
我明确知道需要读取固定长度的数据例如:
固定读取 8 字节
固定读取 32 字节
固定读取一个已知长度的数据块可以使用:
boost::asio::read()如果你的需求是:
串口来了多少数据我先取多少则更适合:
read_some()发送也是一样。
如果你的需求是:
我要把整个 buffer 全部发送出去通常直接使用:
boost::asio::write()如果你希望:
自己控制每一次底层写操作才会直接使用:
write_some()一句话记住:
read()/write()是“把整个 buffer 处理完”;read_some()/write_some()是“这一次能处理多少就先处理多少”。
示例 3:普通 main() 里异步发送串口数据
程序目标
前面的示例 1 使用:
boost::asio::write()同步发送数据。
这一节把它改成异步版本:
boost::asio::async_write()程序打开串口 /dev/pts/3,然后异步发送:
hello async另一个终端从 /dev/pts/4 读取数据。
整个执行流程是:
注册 async_write()
↓
async_write() 立即返回
↓
main 继续执行
↓
调用 io.run()
↓
Asio 等待异步发送完成
↓
调用发送完成回调
↓
没有其他异步任务
↓
io.run() 返回完整代码
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <functional>
#include <print>
#include <string>
void on_write(
const boost::system::error_code& ec,
std::size_t bytes_transferred)
{
if (ec)
{
std::println("on_write:发送失败:{}", ec.message());
return;
}
std::println(
"on_write:发送成功,字节数 = {}",
bytes_transferred);
}
int main(int argc, char* argv[])
{
std::string port_name = "/dev/pts/3";
if (argc >= 2)
{
port_name = argv[1];
}
// io_context 是 Asio 的事件循环对象。
// 异步任务需要由 io.run() 驱动。
boost::asio::io_context io;
boost::asio::serial_port serial(io);
boost::system::error_code ec;
std::println("main:准备打开串口 {}", port_name);
serial.open(port_name, ec);
if (ec)
{
std::println("打开串口失败:{}", ec.message());
return 1;
}
serial.set_option(
boost::asio::serial_port_base::baud_rate(115200));
serial.set_option(
boost::asio::serial_port_base::character_size(8));
serial.set_option(
boost::asio::serial_port_base::parity(
boost::asio::serial_port_base::parity::none));
serial.set_option(
boost::asio::serial_port_base::stop_bits(
boost::asio::serial_port_base::stop_bits::one));
serial.set_option(
boost::asio::serial_port_base::flow_control(
boost::asio::serial_port_base::flow_control::none));
std::string msg = "hello async\n";
std::println("main:注册 async_write");
boost::asio::async_write(
serial,
boost::asio::buffer(msg),
std::bind(
on_write,
std::placeholders::_1,
std::placeholders::_2));
std::println("main:async_write() 已返回");
std::println("main:开始 io.run()");
io.run();
std::println("main:io.run() 返回");
std::println("main:程序结束");
return 0;
}编译运行
终端 1:创建虚拟串口。
socat -d -d \
PTY,raw,echo=0,link=/tmp/ttyV0 \
PTY,raw,echo=0,link=/tmp/ttyV1假设得到:
/dev/pts/3
/dev/pts/4终端 2:监听串口另一端。
cat /dev/pts/4终端 3:编译运行程序。
运行输出与时间顺序
程序输出类似:
main:准备打开串口 /dev/pts/3
main:注册 async_write
main:async_write() 已返回
main:开始 io.run()
on_write:发送成功,字节数 = 12
main:io.run() 返回
main:程序结束监听终端输出:
hello async这里最值得注意的是:
main:注册 async_write
main:async_write() 已返回会先出现。
这说明:
boost::asio::async_write(...)本身不会等数据发送完成。
它做的是:
登记一个异步发送任务
↓
告诉 Asio:
“把这些数据发出去,
发完以后调用 on_write()”
↓
async_write() 立即返回真正等待和处理异步事件的是:
io.run();本示例需要注意的点
同步发送:
boost::asio::write(...)流程是:
调用 write()
↓
当前线程阻塞
↓
整个 buffer 发送完成
↓
write() 返回异步发送:
boost::asio::async_write(...)流程是:
调用 async_write()
↓
登记异步发送任务
↓
立即返回
↓
io.run() 驱动任务
↓
发送完成
↓
调用回调所以异步接口通常必须配合:
io.run();如果本例删掉:
io.run();那么程序很快就会走到:
return 0;异步任务将得不到正常的事件循环驱动。
async_write() 的回调参数
本例回调函数:
void on_write(
const boost::system::error_code& ec,
std::size_t bytes_transferred)有两个参数。
第一个:
const boost::system::error_code& ec表示异步操作有没有发生错误。
第二个:
std::size_t bytes_transferred表示实际完成传输的字节数。
因此绑定时:
std::bind(
on_write,
std::placeholders::_1,
std::placeholders::_2)其中:
_1
→ ec
_2
→ bytes_transferred为什么 msg 不能过早销毁?
这里:
std::string msg = "hello async\n";然后:
boost::asio::async_write(
serial,
boost::asio::buffer(msg),
...);boost::asio::buffer(msg) 并不会复制 msg 的数据。
它只是描述:
msg 的内存地址
+
msg 的长度因此在异步写完成之前:
msg 必须仍然存在本例没有问题,因为:
msg 是 main() 的局部变量
↓
async_write()
↓
io.run()
↓
异步写完成
↓
io.run() 返回
↓
main() 最后才结束所以异步发送期间:
msg一直存在。
不能写成这种结构:
void send(boost::asio::serial_port& serial)
{
std::string msg = "hello\n";
boost::asio::async_write(
serial,
boost::asio::buffer(msg),
handler);
}然后立刻让:
send()返回。
因为:
send() 返回
↓
msg 被销毁
↓
异步写可能还没有完成
↓
buffer 指向的内存失效boost::asio::async_write() 说明
boost::asio::async_write(
serial,
boost::asio::buffer(msg),
handler);作用:
异步发送整个指定 buffer。
它不会等待发送完成。
而是:
注册任务
↓
立即返回
↓
io.run() 驱动
↓
整个 buffer 发送完成
↓
执行 handler一句话记住:
async_write()= “我要异步把整个 buffer 发完,完成以后调用回调”。
示例 4:普通 main() 里异步读取串口数据
程序目标
前面的示例 2 使用:
boost::asio::read()同步读取 11 个字节。
这一节把它改成:
boost::asio::async_read()异步读取。
程序打开 /dev/pts/3,然后注册一个:
异步读取 11 字节的任务。
另一个终端发送:
hello asio\n一共 11 个字节。
读取完成后,Asio 调用回调函数。
执行流程:
注册 async_read()
↓
async_read() 立即返回
↓
main 调用 io.run()
↓
等待数据
↓
累计收到 11 字节
↓
调用 on_read()
↓
没有其他异步任务
↓
io.run() 返回完整代码
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <array>
#include <functional>
#include <print>
#include <string>
void on_read(
const boost::system::error_code& ec,
std::size_t bytes_transferred,
std::array<char, 11>* buffer)
{
if (ec)
{
std::println("on_read:读取失败:{}", ec.message());
return;
}
std::string msg(
buffer->data(),
bytes_transferred);
std::println(
"on_read:读取成功,字节数 = {}",
bytes_transferred);
std::println(
"on_read:收到的数据 = {}",
msg);
}
int main(int argc, char* argv[])
{
std::string port_name = "/dev/pts/3";
if (argc >= 2)
{
port_name = argv[1];
}
boost::asio::io_context io;
boost::asio::serial_port serial(io);
boost::system::error_code ec;
std::println("main:准备打开串口 {}", port_name);
serial.open(port_name, ec);
if (ec)
{
std::println("打开串口失败:{}", ec.message());
return 1;
}
serial.set_option(
boost::asio::serial_port_base::baud_rate(115200));
serial.set_option(
boost::asio::serial_port_base::character_size(8));
serial.set_option(
boost::asio::serial_port_base::parity(
boost::asio::serial_port_base::parity::none));
serial.set_option(
boost::asio::serial_port_base::stop_bits(
boost::asio::serial_port_base::stop_bits::one));
serial.set_option(
boost::asio::serial_port_base::flow_control(
boost::asio::serial_port_base::flow_control::none));
std::array<char, 11> buffer{};
std::println("main:注册 async_read,等待接收 11 个字节");
boost::asio::async_read(
serial,
boost::asio::buffer(buffer),
std::bind(
on_read,
std::placeholders::_1,
std::placeholders::_2,
&buffer));
std::println("main:async_read() 已返回");
std::println("main:开始 io.run()");
io.run();
std::println("main:io.run() 返回");
std::println("main:程序结束");
return 0;
}编译运行
终端 1:创建虚拟串口。
socat -d -d \
PTY,raw,echo=0,link=/tmp/ttyV0 \
PTY,raw,echo=0,link=/tmp/ttyV1终端 2:编译运行程序。
程序会输出:
main:准备打开串口 /dev/pts/3
main:注册 async_read,等待接收 11 个字节
main:async_read() 已返回
main:开始 io.run()此时程序停留在:
io.run();等待异步操作完成。
终端 3:
echo "hello asio" > /dev/pts/4运行输出与时间顺序
完整输出类似:
main:准备打开串口 /dev/pts/3
main:注册 async_read,等待接收 11 个字节
main:async_read() 已返回
main:开始 io.run()
on_read:读取成功,字节数 = 11
on_read:收到的数据 = hello asio
main:io.run() 返回
main:程序结束这里最重要的是:
boost::asio::async_read(...)不会在这里阻塞。
所以:
async_read()
↓
立即返回
↓
继续执行 main
↓
io.run()
↓
等待读取完成本示例需要注意的点
同步读取:
boost::asio::read(...)是:
调用 read()
↓
当前线程阻塞
↓
buffer 被填满
↓
read() 返回异步读取:
boost::asio::async_read(...)是:
登记一个读取任务
↓
立即返回
↓
io.run() 驱动任务
↓
读取完成
↓
调用回调本例 buffer:
std::array<char, 11> buffer{};所以:
boost::asio::async_read(
serial,
boost::asio::buffer(buffer),
...);表示:
异步等待,直到这个 11 字节 buffer 被填满或者发生错误。
如果只发送:
hello异步操作不会完成。
io.run() 仍然会继续等待。
直到累计收到:
11 字节以后:
on_read()才会被调用。
为什么把 &buffer 额外传给回调?(很重要,要记得)
Asio 在异步读取完成时,会自动给回调传入:
const boost::system::error_code& ec
std::size_t bytes_transferred但是它不会自动把:
buffer作为回调参数传给你。
所以我们使用:
std::bind(
on_read,
std::placeholders::_1,
std::placeholders::_2,
&buffer)相当于以后调用:
on_read(
ec,
bytes_transferred,
&buffer);因此回调就可以通过:
buffer->data()访问真正收到的数据。
buffer 生命周期
这里:
std::array<char, 11> buffer{};必须保证在:
async_read()完成之前一直存在。
本例中:
buffer 创建于 main()
↓
注册 async_read()
↓
io.run()
↓
读取完成
↓
on_read()
↓
io.run() 返回
↓
main() 最后结束所以 buffer 的生命周期足够长。
这一点和异步发送中的:
msg是同一个问题:
异步操作使用的底层数据,在操作完成以前不能被销毁。
boost::asio::async_read() 说明
boost::asio::async_read(
serial,
boost::asio::buffer(buffer),
handler);作用:
异步读取,直到指定的整个 buffer 被填满或者发生错误。
它本身立即返回。
真正的读取工作由:
io.run();驱动。
完成以后调用:
handler(ec, bytes_transferred);一句话记住:
async_read()= “我要异步把整个 buffer 读满,完成以后调用回调”。
示例 5:类里封装异步串口写队列
程序目标
前面的示例 3 已经可以异步发送一条数据:
boost::asio::async_write(...)但是实际程序通常不会只发送一次。
例如可能连续调用:
async_send("message one\n");
async_send("message two\n");
async_send("message three\n");如果每次都直接启动一个新的:
async_write()那么多个异步写操作可能同时操作同一个串口。
更稳妥的方式是建立:
发送队列规则:
新数据到来
↓
放进 write_queue_
如果当前没有正在发送的数据
↓
立即开始发送队首
如果当前已经有数据正在发送
↓
只加入队列,不再启动第二个写操作
当前发送完成
↓
删除队首
↓
继续发送下一条最终保证:
同一时刻
只有一个 async_write()
正在操作串口完整代码
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <deque>
#include <functional>
#include <print>
#include <string>
class SerialWriter
{
public:
SerialWriter(
boost::asio::io_context& io,
const std::string& port_name)
: serial_(io)
{
boost::system::error_code ec;
serial_.open(port_name, ec);
if (ec)
{
std::println(
"打开串口失败:{}",
ec.message());
return;
}
serial_.set_option(
boost::asio::serial_port_base::baud_rate(115200));
serial_.set_option(
boost::asio::serial_port_base::character_size(8));
serial_.set_option(
boost::asio::serial_port_base::parity(
boost::asio::serial_port_base::parity::none));
serial_.set_option(
boost::asio::serial_port_base::stop_bits(
boost::asio::serial_port_base::stop_bits::one));
serial_.set_option(
boost::asio::serial_port_base::flow_control(
boost::asio::serial_port_base::flow_control::none));
std::println("SerialWriter:串口打开成功");
}
void async_send(const std::string& msg)
{
// 队列原来非空,说明当前已经有写操作正在进行,
// 或者已经有数据排队等待。
bool writing = !write_queue_.empty();
// 先把数据保存到队列中。
// 这样可以保证异步写期间字符串一直存在。
write_queue_.push_back(msg);
std::print(
"加入发送队列:{}",
msg);
// 原来队列为空,说明当前没有正在执行的写操作。
// 此时由这次调用启动整个发送链。
if (!writing)
{
do_write();
}
}
private:
void do_write()
{
// 永远只发送队首数据。
boost::asio::async_write(
serial_,
boost::asio::buffer(
write_queue_.front()),
std::bind(
&SerialWriter::on_write,
this,
std::placeholders::_1,
std::placeholders::_2));
}
void on_write(
const boost::system::error_code& ec,
std::size_t bytes_transferred)
{
if (ec)
{
std::println(
"发送失败:{}",
ec.message());
return;
}
std::println(
"发送完成,字节数 = {}",
bytes_transferred);
// 当前队首已经发送完成,可以删除。
write_queue_.pop_front();
// 如果后面还有数据,继续发送新的队首。
if (!write_queue_.empty())
{
do_write();
}
}
private:
boost::asio::serial_port serial_;
// 保存等待发送以及当前正在发送的数据。
std::deque<std::string> write_queue_;
};
int main(int argc, char* argv[])
{
std::string port_name = "/dev/pts/3";
if (argc >= 2)
{
port_name = argv[1];
}
boost::asio::io_context io;
SerialWriter writer(io, port_name);
writer.async_send("message one\n");
writer.async_send("message two\n");
writer.async_send("message three\n");
std::println("main:开始 io.run()");
io.run();
std::println(
"main:发送队列处理完成,程序结束");
return 0;
}编译运行
终端 1:
socat -d -d \
PTY,raw,echo=0,link=/tmp/ttyV0 \
PTY,raw,echo=0,link=/tmp/ttyV1终端 2:监听串口另一端。
cat /dev/pts/4再编译执行程序
运行输出与时间顺序
程序输出类似:
SerialWriter:串口打开成功
加入发送队列:message one
加入发送队列:message two
加入发送队列:message three
main:开始 io.run()
发送完成,字节数 = 12
发送完成,字节数 = 12
发送完成,字节数 = 14
main:发送队列处理完成,程序结束
监听终端:
message one
message two
message three
第一次调用 async_send() 时发生了什么?
第一次:
writer.async_send("message one\n");此时:
write_queue_.empty()为:
true所以:
bool writing = !write_queue_.empty();得到:
writing = false然后:
write_queue_.push_back(msg);队列变成:
[ message one ]因为:
writing = false说明原来没有正在执行的发送任务。
所以:
do_write();开始异步发送:
message one第二次和第三次调用时发生什么?
第二次:
writer.async_send("message two\n");此时队列已经是:
[ message one ]因此:
writing = true然后追加:
[ message one ]
[ message two ]但不会再次调用:
do_write();因为:
message one已经在异步发送。
第三次也是一样:
[ message one ] ← 正在发送
[ message two ] ← 等待
[ message three ] ← 等待第一条发送完成后发生什么?
第一条:
message one发送完成以后:
on_write(...)被调用。
首先:
write_queue_.pop_front();删除已经完成的:
message one队列变成:
[ message two ]
[ message three ]然后:
if (!write_queue_.empty())
{
do_write();
}开始发送:
message two所以整个过程就是:
message one
↓
发送完成
↓
pop_front()
message two
↓
发送完成
↓
pop_front()
message three
↓
发送完成
↓
pop_front()
队列为空
↓
不再 do_write()
↓
没有其他异步任务
↓
io.run() 返回为什么必须保存发送数据?
这里不能写成:
void async_send()
{
std::string msg = "hello\n";
boost::asio::async_write(
serial_,
boost::asio::buffer(msg),
...);
}因为:
async_send() 返回
↓
msg 被销毁
↓
async_write 可能还没有完成
↓
buffer 指向失效内存本例使用:
std::deque<std::string> write_queue_;把数据保存起来。
只要当前数据还没有发送完成:
它就在 deque 中发送完成以后:
write_queue_.pop_front();才真正删除。
所以 buffer 生命周期是安全的。
为什么异步写串口通常需要队列?
如果程序连续产生很多数据:
数据 A
数据 B
数据 C我们希望:
async_write(A)
↓
完成
↓
async_write(B)
↓
完成
↓
async_write(C)而不是:
async_write(A)
async_write(B)
async_write(C)
三个写操作同时进行所以写队列的核心作用是:
保证同一个串口上的多次异步发送按顺序一个一个执行。
一句话记住:
async_send()负责“入队”,do_write()负责“发送队首”,on_write()负责“发送完成后删除队首并启动下一条”。
示例 6:类里封装连续异步串口读取
程序目标
前面的示例 4:
boost::asio::async_read(...)只注册了一次异步读取。
读取完成以后:
on_read()
↓
没有新的异步任务
↓
io.run() 返回
↓
程序结束真实程序通常需要:
一直读取串口因此可以把:
serial_port
buffer
async_read()
回调全部封装进一个类。
每次读取完成以后:
on_read()最后再次调用:
start_read();从而形成:
start_read()
↓
等待 11 字节
↓
on_read()
↓
处理数据
↓
start_read()
↓
等待下一组 11 字节
↓
...完整代码
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <array>
#include <functional>
#include <print>
#include <string>
class SerialReader
{
public:
SerialReader(
boost::asio::io_context& io,
const std::string& port_name)
: serial_(io)
{
boost::system::error_code ec;
serial_.open(port_name, ec);
if (ec)
{
std::println(
"打开串口失败:{}",
ec.message());
return;
}
serial_.set_option(
boost::asio::serial_port_base::baud_rate(115200));
serial_.set_option(
boost::asio::serial_port_base::character_size(8));
serial_.set_option(
boost::asio::serial_port_base::parity(
boost::asio::serial_port_base::parity::none));
serial_.set_option(
boost::asio::serial_port_base::stop_bits(
boost::asio::serial_port_base::stop_bits::one));
serial_.set_option(
boost::asio::serial_port_base::flow_control(
boost::asio::serial_port_base::flow_control::none));
std::println(
"SerialReader:串口打开成功,开始异步读取");
start_read();
}
private:
void start_read()
{
std::println(
"SerialReader:等待下一组 11 字节");
boost::asio::async_read(
serial_,
boost::asio::buffer(buffer_),
std::bind(
&SerialReader::on_read,
this,
std::placeholders::_1,
std::placeholders::_2));
}
void on_read(
const boost::system::error_code& ec,
std::size_t bytes_transferred)
{
if (ec)
{
std::println(
"SerialReader::on_read 错误:{}",
ec.message());
return;
}
std::string msg(
buffer_.data(),
bytes_transferred);
std::println(
"收到数据,字节数 = {},内容 = {}",
bytes_transferred,
msg);
// 当前数据处理完成以后,
// 再注册下一次异步读取。
start_read();
}
private:
boost::asio::serial_port serial_;
// 每次固定读取 11 个字节。
std::array<char, 11> buffer_{};
};
int main(int argc, char* argv[])
{
std::string port_name = "/dev/pts/3";
if (argc >= 2)
{
port_name = argv[1];
}
boost::asio::io_context io;
SerialReader reader(io, port_name);
std::println(
"main:调用 io.run(),按 Ctrl+C 结束程序");
io.run();
std::println("main:io.run() 返回");
return 0;
}编译运行
终端 1:
socat -d -d \
PTY,raw,echo=0,link=/tmp/ttyV0 \
PTY,raw,echo=0,link=/tmp/ttyV1程序编译运行
程序输出:
SerialReader:串口打开成功,开始异步读取
SerialReader:等待下一组 11 字节
main:调用 io.run(),按 Ctrl+C 结束程序终端 2 可以连续发送:
printf 'hello asio\n' > /dev/pts/4
printf 'serial c++\n' > /dev/pts/4
printf 'async asio\n' > /dev/pts/4这三组数据都是 11 字节。
运行输出与时间顺序
程序可能输出:
SerialReader:串口打开成功,开始异步读取
SerialReader:等待下一组 11 字节
main:调用 io.run(),按 Ctrl+C 结束程序
收到数据,字节数 = 11,内容 = hello asio
SerialReader:等待下一组 11 字节
收到数据,字节数 = 11,内容 = serial c++
SerialReader:等待下一组 11 字节
收到数据,字节数 = 11,内容 = async asio
SerialReader:等待下一组 11 字节

程序不会自动结束。
原因是每次:
on_read()执行到最后都会再次:
start_read();注册新的异步任务。
所以:
io.run();一直有尚未完成的异步读取任务需要等待。
连续异步读取是怎么形成的?
程序刚创建对象:
SerialReader reader(io, port_name);构造函数最后:
start_read();第一次注册:
async_read()
↓
等待 11 字节收到以后:
on_read()
↓
打印数据
↓
start_read()又注册一次:
async_read()
↓
等待下一组 11 字节所以形成:
start_read()
↓
async_read()
↓
等待数据
↓
on_read()
↓
处理数据
↓
start_read()
↓
async_read()
↓
等待下一组数据
↓
...这一句:
start_read();是实现连续异步读取的关键。
如果删掉它:
void on_read(...)
{
...
// 没有 start_read();
}那么程序只读取一次。
第一次:
11 字节读取完成
↓
on_read()
↓
回调结束
↓
没有新的异步任务
↓
io.run() 返回
↓
程序结束为什么 serial_ 和 buffer_ 要做成成员变量?
这里:
boost::asio::serial_port serial_;
std::array<char, 11> buffer_{};都是:
SerialReader 对象的成员变量因此只要:
SerialReader reader还存在:
serial_
buffer_就一直存在。
这很适合异步操作。
因为:
boost::asio::async_read(
serial_,
boost::asio::buffer(buffer_),
...);注册完成以后:
async_read() 已经返回但是 Asio 之后仍然需要继续使用:
serial_
buffer_如果 buffer 是一个很快被销毁的局部变量:
void start_read()
{
std::array<char, 11> buffer{};
boost::asio::async_read(
serial_,
boost::asio::buffer(buffer),
...);
}那么:
start_read() 返回
↓
局部 buffer 被销毁
↓
异步读取还没完成
↓
Asio 使用的内存失效所以类封装异步 I/O 时,经常把:
I/O 对象
buffer
状态
队列都保存成成员变量。
为什么下一次读取会覆盖上一次的 buffer_?
本例始终使用同一个:
std::array<char, 11> buffer_{};第一次读取:
buffer_
hello asio\n进入:
on_read()以后,我们先:
std::string msg(
buffer_.data(),
bytes_transferred);把当前数据复制到:
msg然后处理。
处理完成后:
start_read();再次:
boost::asio::async_read(
serial_,
boost::asio::buffer(buffer_),
...);下一组数据仍然从:
buffer_[0]开始写。
所以:
第一次:
buffer_
↓
hello asio\n下一次:
buffer_
↓
serial c++\n旧内容会被覆盖。
这没有问题,因为:
上一组数据已经在 on_read() 中处理完成
↓
才重新启动下一次读取为什么这里不会出现两个读取同时修改 buffer_?
因为:
start_read();不是不停地连续调用很多次。
而是:
第一次 async_read()
↓
等待完成
↓
on_read()
↓
当前读取已经结束
↓
才 start_read()
↓
注册下一次 async_read()所以任何时刻只有:
一个 async_read()正在使用:
buffer_流程严格是:
读取 1
↓
完成
读取 2
↓
完成
读取 3
↓
完成而不是:
读取 1 ────────┐
读取 2 ────────┼→ 同时操作 buffer_
读取 3 ────────┘成员函数的 std::bind
这里:
std::bind(
&SerialReader::on_read,
this,
std::placeholders::_1,
std::placeholders::_2)其中:
&SerialReader::on_read表示:
要调用 SerialReader 的成员函数 on_read()this表示:
对当前这个 SerialReader 对象调用两个占位符:
_1
→ ec
_2
→ bytes_transferred所以异步读取完成以后,相当于:
this->on_read(
ec,
bytes_transferred);示例 4 和示例 6 的区别
示例 4:
注册一次 async_read()
↓
读取一次
↓
回调一次
↓
结束示例 6:
start_read()
↓
async_read()
↓
on_read()
↓
start_read()
↓
async_read()
↓
on_read()
↓
...所以:
示例 4
→ 学习一次异步读取是怎么工作的
示例 6
→ 学习怎样把异步读取封装进类并持续运行一句话记住:
连续异步读取的核心不是写一个死循环,而是在一次异步读取完成的回调里,再注册下一次异步读取。
示例 7:类里同时封装异步串口发送和接收
程序目标
前面的两个示例分别完成了:
示例 5:
异步发送队列
示例 6:
连续异步接收这一节把它们合并到同一个类中。
最终得到:
AsyncSerialPort
│
├── 接收
│ │
│ ├── start_read()
│ ├── async_read()
│ └── on_read()
│ ↓
│ 再次 start_read()
│
└── 发送
│
├── async_send()
├── write_queue_
├── do_write()
└── on_write()
↓
继续发送队首这个类可以:
一边持续接收串口数据
同时
一边异步发送串口数据本示例仍然只有:
一个线程调用 io.run()所以暂时不涉及多线程并发问题。
完整代码
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <array>
#include <deque>
#include <functional>
#include <print>
#include <string>
class AsyncSerialPort
{
public:
AsyncSerialPort(
boost::asio::io_context& io,
const std::string& port_name)
: serial_(io)
{
boost::system::error_code ec;
serial_.open(port_name, ec);
if (ec)
{
std::println(
"打开串口失败:{}",
ec.message());
return;
}
serial_.set_option(
boost::asio::serial_port_base::baud_rate(115200));
serial_.set_option(
boost::asio::serial_port_base::character_size(8));
serial_.set_option(
boost::asio::serial_port_base::parity(
boost::asio::serial_port_base::parity::none));
serial_.set_option(
boost::asio::serial_port_base::stop_bits(
boost::asio::serial_port_base::stop_bits::one));
serial_.set_option(
boost::asio::serial_port_base::flow_control(
boost::asio::serial_port_base::flow_control::none));
std::println(
"AsyncSerialPort:串口打开成功");
// 注册第一次异步读取。
start_read();
}
void async_send(const std::string& msg)
{
// 队列原来非空,说明已经有数据正在发送,
// 或者已经有其他数据正在排队。
bool writing = !write_queue_.empty();
// 保存发送数据,保证异步写期间内存一直存在。
write_queue_.push_back(msg);
std::print(
"加入发送队列:{}",
msg);
// 如果原来没有正在发送的数据,
// 就从当前队首开始整个发送流程。
if (!writing)
{
do_write();
}
}
private:
void start_read()
{
std::println(
"AsyncSerialPort:等待接收下一组 11 字节");
boost::asio::async_read(
serial_,
boost::asio::buffer(read_buffer_),
std::bind(
&AsyncSerialPort::on_read,
this,
std::placeholders::_1,
std::placeholders::_2));
}
void on_read(
const boost::system::error_code& ec,
std::size_t bytes_transferred)
{
if (ec)
{
std::println(
"读取失败:{}",
ec.message());
return;
}
std::string msg(
read_buffer_.data(),
bytes_transferred);
std::println(
"收到数据,字节数 = {},内容 = {}",
bytes_transferred,
msg);
// 当前这一组数据处理完成以后,
// 再注册下一次异步读取。
start_read();
}
void do_write()
{
boost::asio::async_write(
serial_,
boost::asio::buffer(
write_queue_.front()),
std::bind(
&AsyncSerialPort::on_write,
this,
std::placeholders::_1,
std::placeholders::_2));
}
void on_write(
const boost::system::error_code& ec,
std::size_t bytes_transferred)
{
if (ec)
{
std::println(
"发送失败:{}",
ec.message());
return;
}
std::println(
"发送完成,字节数 = {}",
bytes_transferred);
// 当前队首已经发送完成。
write_queue_.pop_front();
// 如果队列还有数据,继续发送新的队首。
if (!write_queue_.empty())
{
do_write();
}
}
private:
boost::asio::serial_port serial_;
// 每次固定读取 11 个字节。
std::array<char, 11> read_buffer_{};
// 保存正在发送以及等待发送的数据。
std::deque<std::string> write_queue_;
};
int main(int argc, char* argv[])
{
std::string port_name = "/dev/pts/3";
if (argc >= 2)
{
port_name = argv[1];
}
boost::asio::io_context io;
AsyncSerialPort serial(io, port_name);
// 先注册几条异步发送任务。
serial.async_send("message one\n");
serial.async_send("message two\n");
serial.async_send("message three\n");
std::println(
"main:开始 io.run(),按 Ctrl+C 结束程序");
io.run();
std::println("main:io.run() 返回");
return 0;
}编译运行
终端 1:创建虚拟串口。
socat -d -d \
PTY,raw,echo=0,link=/tmp/ttyV0 \
PTY,raw,echo=0,link=/tmp/ttyV1假设得到:
/dev/pts/3
/dev/pts/4终端 2:监听程序发送的数据。
cat /dev/pts/4编译运行程序。
如果还要测试接收,可以再开一个终端向 /dev/pts/4 写入:
printf 'hello asio\n' > /dev/pts/4因为:
hello asio\n正好是 11 字节。


整个类的工作过程
程序创建:
AsyncSerialPort serial(io, port_name);构造函数最后调用:
start_read();因此首先注册一个:
异步接收 11 字节的任务。
与此同时:
serial.async_send("message one\n");
serial.async_send("message two\n");
serial.async_send("message three\n");又建立了发送队列:
message one ← 正在发送
message two ← 等待
message three ← 等待因此:
AsyncSerialPort
│
┌─────────────┴─────────────┐
↓ ↓
接收 发送
↓ ↓
async_read() async_write()
↓ ↓
on_read() on_write()
↓ ↓
再次 async_read() 发送下一条接收和发送可以同时存在。
也就是说:
一个异步读操作
+
一个异步写操作可以同时挂在同一个串口对象上。
为什么这里不需要 mutex?
虽然:
read_buffer_
write_queue_都是成员变量,但是本示例只有:
main 线程调用:
io.run();所以所有异步 handler:
on_read()
on_write()实际上都在同一个线程中一个接一个执行。
不会出现:
线程 1:on_read()
同时
线程 2:on_write()因此这里暂时没有多个线程同时访问:
write_queue_
read_buffer_的问题。
所以不需要:
std::mutex也不需要:
strand本示例最重要的结构
可以把整个类记成:
接收:
start_read()
↓
async_read()
↓
on_read()
↓
处理数据
↓
start_read()发送:
async_send()
↓
加入 write_queue_
↓
do_write()
↓
async_write()
↓
on_write()
↓
pop_front()
↓
继续下一条一句话记住:
一个完整的异步串口类,通常同时包含“持续读取链”和“发送队列链”。
示例 8:主线程产生数据,独立 Asio 线程负责串口通信
这种情况一般适用于一个串口设备。(此时main有1个主线程,asio有1个线程)
程序目标
示例 7 中:
io.run();直接运行在 main 线程。
这意味着:
main 线程
↓
进入 io.run()
↓
长期负责 Asio 事件循环实际程序中,经常希望:
main 线程
→ 继续处理自己的业务逻辑
Asio 线程
→ 专门处理串口收发于是程序变成:
主线程
│
产生需要发送的数据
│
↓
serial.async_send(...)
│
↓
post()
│
↓
================================
Asio 线程
================================
│
↓
write_queue_
│
↓
async_write()
│
↓
串口与此同时串口接收也是:
串口
↓
async_read()
↓
Asio 线程执行 on_read()这样:
main 线程负责“产生任务”,Asio 线程负责“真正处理串口”。
完整代码
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <array>
#include <deque>
#include <functional>
#include <iostream>
#include <print>
#include <string>
#include <thread>
class AsyncSerialPort
{
public:
AsyncSerialPort(
boost::asio::io_context& io,
const std::string& port_name)
: io_(io),
serial_(io)
{
boost::system::error_code ec;
serial_.open(port_name, ec);
if (ec)
{
std::println(
"打开串口失败:{}",
ec.message());
return;
}
serial_.set_option(
boost::asio::serial_port_base::baud_rate(115200));
serial_.set_option(
boost::asio::serial_port_base::character_size(8));
serial_.set_option(
boost::asio::serial_port_base::parity(
boost::asio::serial_port_base::parity::none));
serial_.set_option(
boost::asio::serial_port_base::stop_bits(
boost::asio::serial_port_base::stop_bits::one));
serial_.set_option(
boost::asio::serial_port_base::flow_control(
boost::asio::serial_port_base::flow_control::none));
std::println(
"AsyncSerialPort:串口打开成功");
}
void start()
{
// 不直接在调用 start() 的线程里操作内部状态,
// 而是把 start_read() 投递给 io_context。
boost::asio::post(
io_,
std::bind(
&AsyncSerialPort::start_read,
this));
}
void async_send(const std::string& msg)
{
// main 线程不直接修改 write_queue_。
// 只把“加入发送队列”这个任务投递给 Asio 线程。
boost::asio::post(
io_,
std::bind(
&AsyncSerialPort::enqueue_write,
this,
msg));
}
void stop()
{
// 停止操作也交给 Asio 线程完成。
boost::asio::post(
io_,
std::bind(
&AsyncSerialPort::do_stop,
this));
}
private:
void start_read()
{
if (stopping_ || !serial_.is_open())
{
return;
}
std::println(
"Asio线程:等待下一组 11 字节");
boost::asio::async_read(
serial_,
boost::asio::buffer(read_buffer_),
std::bind(
&AsyncSerialPort::on_read,
this,
std::placeholders::_1,
std::placeholders::_2));
}
void on_read(
const boost::system::error_code& ec,
std::size_t bytes_transferred)
{
if (ec)
{
std::println(
"读取结束:{}",
ec.message());
return;
}
std::string msg(
read_buffer_.data(),
bytes_transferred);
std::println(
"Asio线程:收到数据,字节数 = {},内容 = {}",
bytes_transferred,
msg);
if (!stopping_)
{
start_read();
}
}
void enqueue_write(std::string msg)
{
if (stopping_)
{
return;
}
bool writing = !write_queue_.empty();
write_queue_.push_back(msg);
std::print(
"Asio线程:加入发送队列:{}",
msg);
if (!writing)
{
do_write();
}
}
void do_write()
{
if (stopping_ || write_queue_.empty())
{
return;
}
boost::asio::async_write(
serial_,
boost::asio::buffer(
write_queue_.front()),
std::bind(
&AsyncSerialPort::on_write,
this,
std::placeholders::_1,
std::placeholders::_2));
}
void on_write(
const boost::system::error_code& ec,
std::size_t bytes_transferred)
{
if (ec)
{
std::println(
"发送结束:{}",
ec.message());
return;
}
std::println(
"Asio线程:发送完成,字节数 = {}",
bytes_transferred);
write_queue_.pop_front();
if (!write_queue_.empty())
{
do_write();
}
}
void do_stop()
{
if (stopping_)
{
return;
}
stopping_ = true;
std::println(
"Asio线程:开始关闭串口");
boost::system::error_code ec;
serial_.cancel(ec);
ec.clear();
serial_.close(ec);
}
private:
boost::asio::io_context& io_;
boost::asio::serial_port serial_;
std::array<char, 11> read_buffer_{};
std::deque<std::string> write_queue_;
bool stopping_ = false;
};
void run_io(boost::asio::io_context* io)
{
std::println(
"Asio线程:开始 io.run(),线程 id = {}",
std::this_thread::get_id());
io->run();
std::println(
"Asio线程:io.run() 返回");
}
int main(int argc, char* argv[])
{
std::string port_name = "/dev/pts/3";
if (argc >= 2)
{
port_name = argv[1];
}
boost::asio::io_context io;
// 保证 Asio 后台线程即使暂时没有任务,
// io.run() 也不会立刻返回。
auto guard =
boost::asio::make_work_guard(io);
AsyncSerialPort serial(io, port_name);
// 单独创建一个线程运行 Asio 事件循环。
std::thread io_thread(
std::bind(
run_io,
&io));
// 此时 Asio 线程已经可能进入 io.run()。
// start() 把第一次读取任务投递给 Asio 线程。
serial.start();
std::println(
"main线程 id = {}",
std::this_thread::get_id());
// 模拟 main 中的其他业务逻辑不断产生待发送数据。
serial.async_send("message one\n");
serial.async_send("message two\n");
serial.async_send("message three\n");
std::println(
"main:数据已经提交给 Asio,main 可以继续处理其他工作");
std::println(
"main:按 Enter 结束程序");
std::string input;
std::getline(std::cin, input);
// 把关闭串口任务投递给 Asio 线程。
serial.stop();
// 允许 io.run() 在剩余任务处理完成后自然返回。
guard.reset();
io_thread.join();
std::println("main:程序结束");
return 0;
}编译运行
终端 1:
socat -d -d \
PTY,raw,echo=0,link=/tmp/ttyV0 \
PTY,raw,echo=0,link=/tmp/ttyV1终端 2:监听程序发送的数据。
cat /dev/pts/4也可以看十六进制:
xxd -g 1 /dev/pts/4
# 或者
cat /dev/pts/4 | xxd -g 1运行程序。
如果要测试程序接收数据,可以再打开一个终端:
printf 'hello asio\n' > /dev/pts/4
整个线程关系
现在程序里已经有两个线程:
主线程
Asio线程主线程:
main()
↓
产生数据
↓
serial.async_send()
↓
继续做其他工作Asio 线程:
io.run()
↓
执行串口相关 handler
↓
读串口
↓
写串口
↓
维护 write_queue_可以画成:
主线程
│
├── async_send("message one")
│
├── async_send("message two")
│
└── async_send("message three")
│
│ post()
↓
================================
Asio线程
================================
│
↓
enqueue_write()
↓
write_queue_
↓
do_write()
↓
async_write()所以:
async_send()并不意味着 main 线程亲自操作串口。
它只是把任务交给:
Asio线程去执行。
为什么要使用 post()?
这是本示例非常重要的一点。
假设我们不使用:
boost::asio::post(...)而是让:
async_send()直接执行:
write_queue_.push_back(msg);就会出现:
main线程
↓
write_queue_.push_back()
同时
Asio线程
↓
write_queue_.pop_front()也就是两个线程同时操作:
write_queue_这会产生:
data race如果直接共享 write_queue_,就需要 mutex
传统解决办法是:
std::mutex mutex_;然后 main 线程修改队列:
{
std::scoped_lock lock(mutex_);
write_queue_.push_back(msg);
}Asio 线程删除队首:
{
std::scoped_lock lock(mutex_);
write_queue_.pop_front();
}于是:
main线程 ─────┐
│
↓
mutex
↓
write_queue_
↑
│
Asio线程 ─────┘这样同一时刻只有一个线程能访问队列。
这就是:
mutex最典型的用途:
多个线程必须直接访问同一个共享可变数据时,用锁保护临界区。
但本例为什么没有使用 mutex?
因为我们改变了设计。
main 线程不再直接碰:
write_queue_main 线程只执行:
boost::asio::post(...)真正的:
write_queue_.push_back()发生在:
enqueue_write()里面。
而 enqueue_write() 是由:
唯一的 Asio线程执行的。
同时:
on_write()也是这个 Asio 线程执行。
所以:
main线程
↓
只 post()
↓
不访问 write_queue_
Asio线程
↓
push_back()
pop_front()
front()
↓
所有队列操作都由同一个线程完成因此:
没有两个线程同时访问 write_queue_自然也就不需要:
mutex这是一个非常重要的并发设计思想:
能通过“让某个状态只属于一个线程”解决的问题,就不一定非要让多个线程共享它再到处加锁。
post() 可以理解成什么?
例如:
serial.async_send("message one\n");内部:
boost::asio::post(
io_,
std::bind(
&AsyncSerialPort::enqueue_write,
this,
msg));可以简单理解成:
main线程:
“这里有一个任务,
以后请 io_context 帮我执行。”
↓
post()
↓
立即返回然后:
Asio线程:
io.run()
↓
发现一个 post 过来的任务
↓
执行 enqueue_write()所以:
post()非常适合:
其他线程
↓
向 Asio 线程提交任务work_guard 在这里为什么特别重要?
程序首先:
auto guard =
boost::asio::make_work_guard(io);然后启动:
std::thread io_thread(
std::bind(
run_io,
&io));此时存在一种可能:
Asio线程启动得非常快
↓
进入 io.run()
↓
serial.start() 还没来得及 post
↓
当前没有任务如果没有:
work_guard那么:
io.run()
↓
发现没有工作
↓
直接返回
↓
Asio线程结束随后 main 再:
serial.start();
serial.async_send(...);虽然任务被投递进:
io_context但是已经没有线程执行:
io.run()任务就不会被处理。
有了:
work_guard流程变成:
Asio线程
↓
io.run()
↓
暂时没有任务
↓
但 guard 还在
↓
不能退出
↓
继续等待
↓
main post 新任务
↓
开始执行所以这里:
work_guard真正体现出了:
后台 Asio 线程暂时没任务时也不要退出,后面其他线程还可能继续投递任务。
为什么结束时先 stop(),再 guard.reset()?
程序退出时:
serial.stop();
guard.reset();
io_thread.join();其中:
serial.stop();并不是直接由 main 线程关闭串口。
它内部:
post(...)把:
关闭串口这个任务交给 Asio 线程。
然后:
guard.reset();表示:
不再人为保持
io.run()存活。
Asio 线程处理完:
关闭串口
取消异步读取
处理剩余 handler以后,如果没有其他工作:
io.run()
↓
返回最后:
io_thread.join();等待 Asio 线程真正结束。
所以退出流程:
main
↓
post(stop)
↓
guard.reset()
↓
等待 join()
Asio线程
↓
处理 stop
↓
取消串口异步任务
↓
没有其他工作
↓
io.run() 返回
↓
线程结束示例 8 最重要的架构
可以直接记成:
业务线程 / main线程
│
│ async_send()
↓
post()
│
↓
==============================
Asio通信线程
==============================
│
├── serial_port
├── read_buffer_
├── write_queue_
├── async_read()
└── async_write()这样:
业务逻辑和:
串口 I/O就被放到了不同线程。
同时利用:
post()避免 main 线程和 Asio 线程直接共同修改串口内部状态。
一句话记住:
一个独立 Asio 通信线程时,可以让其他线程只通过
post()提交任务,而把串口、buffer、发送队列等内部状态全部交给 Asio 线程管理,从而减少对mutex的需求。
示例 9:多个线程同时运行 io.run(),使用 strand 避免 handler 并发
这种情况一般适用于多个通信设备。(此时main有1个主线程,asio有2+个线程)
程序目标
示例 8 中只有:
一个 Asio线程执行:
io.run();因此:
enqueue_write()
on_write()
on_read()这些 handler 本身不会同时执行。
但是如果我们为了让:
io_context拥有更强的并发处理能力,改成:
Asio线程1 → io.run()
Asio线程2 → io.run()那么:
不同 handler就可能在两个线程上同时执行。
例如:
Asio线程1
↓
enqueue_write()
↓
write_queue_.push_back()
Asio线程2
↓
on_write()
↓
write_queue_.pop_front()如果同时发生,就又产生了数据竞争。
这一节使用:
strand解决这个问题。
核心规则:
绑定到同一个
strand的 handler 不会彼此并发执行。
完整代码
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <array>
#include <deque>
#include <functional>
#include <iostream>
#include <print>
#include <string>
#include <thread>
class AsyncSerialPort
{
public:
AsyncSerialPort(
boost::asio::io_context& io,
const std::string& port_name)
: strand_(boost::asio::make_strand(io)),
serial_(io)
{
boost::system::error_code ec;
serial_.open(port_name, ec);
if (ec)
{
std::println(
"打开串口失败:{}",
ec.message());
return;
}
serial_.set_option(
boost::asio::serial_port_base::baud_rate(115200));
serial_.set_option(
boost::asio::serial_port_base::character_size(8));
serial_.set_option(
boost::asio::serial_port_base::parity(
boost::asio::serial_port_base::parity::none));
serial_.set_option(
boost::asio::serial_port_base::stop_bits(
boost::asio::serial_port_base::stop_bits::one));
serial_.set_option(
boost::asio::serial_port_base::flow_control(
boost::asio::serial_port_base::flow_control::none));
std::println(
"AsyncSerialPort:串口打开成功");
}
void start()
{
// start_read() 也通过 strand 执行。
boost::asio::post(
strand_,
std::bind(
&AsyncSerialPort::start_read,
this));
}
void async_send(const std::string& msg)
{
// 所有修改发送队列的任务,
// 都先投递到同一个 strand。
boost::asio::post(
strand_,
std::bind(
&AsyncSerialPort::enqueue_write,
this,
msg));
}
void stop()
{
boost::asio::post(
strand_,
std::bind(
&AsyncSerialPort::do_stop,
this));
}
private:
void start_read()
{
if (stopping_ || !serial_.is_open())
{
return;
}
boost::asio::async_read(
serial_,
boost::asio::buffer(read_buffer_),
// 读取完成的 handler 也绑定到同一个 strand。
boost::asio::bind_executor(
strand_,
std::bind(
&AsyncSerialPort::on_read,
this,
std::placeholders::_1,
std::placeholders::_2)));
}
void on_read(
const boost::system::error_code& ec,
std::size_t bytes_transferred)
{
if (ec)
{
std::println(
"读取结束:{},线程 id = {}",
ec.message(),
std::this_thread::get_id());
return;
}
std::string msg(
read_buffer_.data(),
bytes_transferred);
std::println(
"收到数据,bytes = {},线程 id = {},内容 = {}",
bytes_transferred,
std::this_thread::get_id(),
msg);
if (!stopping_)
{
start_read();
}
}
void enqueue_write(std::string msg)
{
if (stopping_)
{
return;
}
std::println(
"enqueue_write(),线程 id = {}",
std::this_thread::get_id());
bool writing =
!write_queue_.empty();
write_queue_.push_back(msg);
if (!writing)
{
do_write();
}
}
void do_write()
{
if (stopping_ || write_queue_.empty())
{
return;
}
boost::asio::async_write(
serial_,
boost::asio::buffer(
write_queue_.front()),
// 写完成 handler 同样绑定到 strand。
boost::asio::bind_executor(
strand_,
std::bind(
&AsyncSerialPort::on_write,
this,
std::placeholders::_1,
std::placeholders::_2)));
}
void on_write(
const boost::system::error_code& ec,
std::size_t bytes_transferred)
{
if (ec)
{
std::println(
"发送结束:{},线程 id = {}",
ec.message(),
std::this_thread::get_id());
return;
}
std::println(
"发送完成,bytes = {},线程 id = {}",
bytes_transferred,
std::this_thread::get_id());
write_queue_.pop_front();
if (!write_queue_.empty())
{
do_write();
}
}
void do_stop()
{
if (stopping_)
{
return;
}
stopping_ = true;
std::println(
"关闭串口,线程 id = {}",
std::this_thread::get_id());
boost::system::error_code ec;
serial_.cancel(ec);
ec.clear();
serial_.close(ec);
}
private:
boost::asio::strand<
boost::asio::io_context::executor_type>
strand_;
boost::asio::serial_port serial_;
std::array<char, 11> read_buffer_{};
std::deque<std::string> write_queue_;
bool stopping_ = false;
};
void run_io(
boost::asio::io_context* io,
const std::string& name)
{
std::println(
"{}:开始 io.run(),线程 id = {}",
name,
std::this_thread::get_id());
io->run();
std::println(
"{}:io.run() 返回",
name);
}
int main(int argc, char* argv[])
{
std::string port_name = "/dev/pts/3";
if (argc >= 2)
{
port_name = argv[1];
}
boost::asio::io_context io;
auto guard =
boost::asio::make_work_guard(io);
AsyncSerialPort serial(io, port_name);
// 两个线程同时运行同一个 io_context。
std::thread t1(
std::bind(
run_io,
&io,
std::string("Asio线程1")));
std::thread t2(
std::bind(
run_io,
&io,
std::string("Asio线程2")));
serial.start();
serial.async_send("message one\n");
serial.async_send("message two\n");
serial.async_send("message three\n");
std::println(
"main:两个 Asio 线程正在运行,按 Enter 结束");
std::string input;
std::getline(std::cin, input);
serial.stop();
guard.reset();
t1.join();
t2.join();
std::println("main:程序结束");
return 0;
}编译运行
终端 1:
socat -d -d \
PTY,raw,echo=0,link=/tmp/ttyV0 \
PTY,raw,echo=0,link=/tmp/ttyV1终端 2:
cat /dev/pts/4编译并运行程序
如果测试接收:
printf 'hello asio\n' > /dev/pts/4为什么示例 8 不需要 strand,示例 9 却需要?
示例 8:
只有一个线程
↓
io.run()因此所有 Asio handler:
handler A
↓
handler B
↓
handler C只能一个一个执行。
即使:
boost::asio::post(io, handler);投递了很多任务,它们最终还是由:
唯一一个 Asio线程执行。
所以:
write_queue_
read_buffer_
stopping_不会被两个 Asio handler 同时访问。
示例 9:
Asio线程1 → io.run()
Asio线程2 → io.run()此时 io_context 可以同时调度两个 handler。
例如:
时间 ─────────────────────────→
线程1:[ enqueue_write() ]
线程2: [ on_write() ]
↑
发生重叠两个函数都可能操作:
write_queue_于是产生数据竞争。
strand 在这里发挥什么作用?
创建:
strand_(boost::asio::make_strand(io))然后所有与:
AsyncSerialPort 内部状态有关的 handler 都放到:
strand_上。
例如:
boost::asio::post(
strand_,
handler);以及:
boost::asio::bind_executor(
strand_,
handler);于是:
enqueue_write() ─┐
on_write() ├── 同一个 strand
on_read() │
do_stop() ┘strand 保证:
这些 handler 不会彼此并发执行。
所以即使:
线程1
线程2都在执行:
io.run();也只能出现:
线程1:
[ enqueue_write() ]
线程2:
[ on_write() ]或者:
线程2:
[ on_write() ]
线程1:
[ on_read() ]而不会:
线程1:
[ enqueue_write() ]
线程2:
[ on_write() ]
↑
重叠执行strand 不是把串口固定到某一个线程
这一点非常重要。
可能第一次:
on_read()在线程 1 执行。
下一次:
on_read()在线程 2 执行。
例如输出可能是:
收到数据,线程 id = 111111
发送完成,线程 id = 222222
收到数据,线程 id = 222222
发送完成,线程 id = 111111这是完全正常的。
strand 保证的是:
执行时间不重叠而不是:
永远固定在线程1所以:
strand
≠ 固定线程
strand
= 串行化一组 handler为什么不仅 on_read() / on_write() 要绑定 strand?
假设我们只写:
boost::asio::bind_executor(
strand_,
on_write_handler);但是:
async_send()仍然:
boost::asio::post(
io,
enqueue_write_handler);那么:
enqueue_write()没有经过 strand。
两个 Asio 线程仍然可能出现:
线程1:
enqueue_write()
↓
push_back()
线程2:
on_write()
↓
pop_front()所以还不安全。
因此只要一个操作会访问:
AsyncSerialPort 的共享内部状态就应该让它遵守同一个:
strand_本例因此统一使用:
start()
↓
post(strand_)
async_send()
↓
post(strand_)
stop()
↓
post(strand_)
on_read()
↓
bind_executor(strand_)
on_write()
↓
bind_executor(strand_)这样:
write_queue_
read_buffer_
stopping_
serial_ 的异步状态变化都处于同一套串行化规则中。
那这里能不能不用 strand,改用 mutex?
可以。
例如:
std::mutex mutex_;然后每次访问:
write_queue_都:
std::scoped_lock lock(mutex_);理论上也可以保护共享状态。
但是这个类的情况是:
大量内部状态变化
本来就是由 Asio handler 驱动包括:
enqueue_write()
on_write()
on_read()
stop()因此直接规定:
这些 handler 全部通过同一个 strand 串行执行。
通常比:
这里 lock
那里 unlock
这个变量锁一下
那个状态再考虑另一个锁更加自然。
可以这样区分:
mutex
→ 保护某一段共享代码 / 数据
strand
→ 串行化一组 Asio handler示例 8 和示例 9 的核心区别
示例 8:
主线程
+
1 个 Asio线程主线程通过:
post()把串口相关任务交给唯一的 Asio 线程。
于是:
串口内部状态
→ 只有一个线程真正操作
→ 不需要 mutex
→ 不需要 strand示例 9:
主线程
+
2 个 Asio线程两个 Asio 线程都执行:
io.run();于是不同 handler 可能并发。
所以:
串口内部 handler
↓
统一进入 strand
↓
保证不并发整个学习过程可以记成:
示例 7
单线程
↓
完整异步收发类
示例 8
主线程 + 1 个 Asio线程
↓
post()
↓
线程间任务传递
↓
顺便理解 mutex 为什么会出现
示例 9
主线程 + 多个 Asio线程
↓
handler 开始可能并发
↓
strand
↓
串行化串口内部状态一句话记住:
mutex解决的是“多个线程共同访问共享数据”;post()可以通过线程所有权设计减少这种共享;而当多个线程同时运行同一个io_context时,strand可以进一步保证一组 Asio handler 不会彼此并发执行。
Standalone Asio
Standalone Asio ≈ 把 Boost.Asio 从 Boost 体系里单独拿出来,改用标准库类型。 两者本质上是同一套 Asio 技术路线,作者和核心实现也基本同源;Standalone Asio 的代码会被周期性转换成 Boost.Asio。
一般来讲,boost库太大太重了,而且boost库里面很多内容比如error_code,thread等早就被ISO C++收编到标准库了。
咱们只需要用到boost里的asio的功能,所以在实际开发里,直接用Standalone Asio即可。
这俩没有任何性能差距,因为 Standalone Asio 和 Boost.Asio 本来就是同源实现,官方说明 Boost.Asio 是从 Asio 源码周期性转换而来的。
| 对比项 | Standalone Asio | Boost.Asio |
|---|---|---|
| 头文件 | <asio.hpp> | <boost/asio.hpp> |
| 命名空间 | asio:: | boost::asio:: |
| 错误类型 | std::error_code / asio::error_code | boost::system::error_code |
| Boost 依赖 | 基本不需要 | 属于 Boost,使用 Boost.System |
io_context | asio::io_context | boost::asio::io_context |
serial_port | asio::serial_port | boost::asio::serial_port |
strand | asio::strand | boost::asio::strand |
post / dispatch | 有 | 有 |
coroutine / co_spawn | 有 | 有 |
| 性能 | 基本相同 | 基本相同 |
| API 风格 | 几乎相同 | 几乎相同 |
| 独立库发布 | 是 | 随 Boost 发布 |
| 适合减少依赖 | 更好 | 一般 |
代码对比:
#include <boost/asio.hpp>
boost::asio::io_context io_context_;
boost::asio::serial_port serial_port_{io_context_};
boost::asio::post(io_context_, [this]() {
// send
});#include <asio.hpp>
asio::io_context io_context_;
asio::serial_port serial_port_{io_context_};
asio::post(io_context_, [this]() {
// send
});错误处理:
从boost::system::error_code ec;换成asio::error_code ec;或者std::error_code(更建议用标准库,实际上就是boost库被收编的内容)
然后在版本上,拿Ubuntu 22.04 Jammy举例子:
Standalone Asio的版本是libasio-dev = 1.18.1。
Boost的版本是libboost-dev 1.74,但里面的Boost.Asio的版本是Asio 1.18.0。
所以几乎就是同一代,几乎没有任何差距。
其他的全部可以原封不动保留。

