第 23.1.3 节

串口通信教学

作者
董佳辉的头像董佳辉
发布于 2023/10/05更新于 2026/10/06

串口是机器人里最常见的通信方式之一:上位机 ROS2 / Linux 程序通过 USB-TTL、USB-CAN 转串口、CH340、CP2102、FT232 等设备和 STM32、ESP32、下位机控制板通信。

本节依旧遵循顺序:

先写普通 main() 里的同步示例
再写普通 main() 里的异步示例
最后写类封装版本

所有异步回调统一使用 std::bind。


Linux 下准备虚拟串口测试环境

如果你手里没有真实 STM32,可以用 socat 创建一对虚拟串口。

安装 socat

Ubuntu / Debian:

sudo apt install socat

Fedora:

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/3
ec

用于接收错误信息。

如果打开失败:

if (ec)
{
    std::println("打开串口失败:{}", ec.message());
    return 1;
}

serial.set_option(...)

作用:设置串口参数,例如波特率、数据位、校验位、停止位和流控。

本示例设置:

波特率:115200
数据位:8
校验位:无
停止位:1
流控:无

也就是常见的:

115200 8N1

std::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 AsioBoost.Asio
头文件<asio.hpp><boost/asio.hpp>
命名空间asio::boost::asio::
错误类型std::error_code / asio::error_codeboost::system::error_code
Boost 依赖基本不需要属于 Boost,使用 Boost.System
io_contextasio::io_contextboost::asio::io_context
serial_portasio::serial_portboost::asio::serial_port
strandasio::strandboost::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。

所以几乎就是同一代,几乎没有任何差距。

其他的全部可以原封不动保留。