跳到正文

Wiki

串口通信教學

約 25 分鐘閱讀

本文由簡體中文內容確定性轉換,並受版本化術語表保護。

串口是機械人裏最常見的通信方式之一:上位機 ROS2 / Linux 程序通過 USB-TTL、USB-CAN 轉串口、CH340、CP2102、FT232 等設備和 STM32、ESP32、下位機控制板通信。

本節依舊遵循順序:

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

所有異步回調統一使用 std::bind。


1.Linux 下準備虛擬串口測試環境

如果你手裏沒有真實 STM32,可以用 socat 創建一對虛擬串口。

1.1.安裝 socat

Ubuntu / Debian:

sudo apt install socat

Fedora:

sudo dnf install socat

1.2.創建一對虛擬串口

新開一個終端,運行:

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。你要按自己終端實際輸出修改。


2.示例 1:普通 main() 裏同步打開串口併發送一行

2.1.程序目標

程序打開串口 /dev/pts/3,設置波特率 115200,然後發送:

hello serial

另一個終端從 /dev/pts/4 讀到這行數據。

2.2.完整代碼

#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;
}

運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。

2.3.編譯運行

終端 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來發送數據。

2.4.運行輸出與時間順序

程序終端輸出:

main:准备打开串口 /dev/pts/3
main:发送成功,字节数 = 13
main:程序结束

監聽終端輸出:

hello serial

這個示例基本沒有明顯延遲,發送完成後程序立刻結束。

2.5.本示例需要注意的點

這是同步寫串口。boost::asio::write() 會阻塞,直到數據寫完或發生錯誤。

2.6.關鍵函數説明

2.6.1.serial.open(port_name, ec)

作用:打開串口設備。

參數:

port_name

例如 /dev/ttyUSB0、/dev/ttyACM0、/dev/pts/3。

ec

用於接收錯誤信息。這樣寫不會拋異常,更適合教程和機械人工程。

2.6.2.serial.set_option(...)

作用:設置串口參數,例如波特率、數據位、校驗位、停止位、流控。

2.6.3.boost::asio::write(serial, boost::asio::buffer(msg), ec)

作用:同步寫數據。(阻塞式發數據)

返回值:實際寫入的字節數。


3.示例 2:普通 main() 裏同步讀取串口數據

3.1.程序目標

程序打開串口 /dev/pts/3,設置波特率 115200,然後阻塞等待接收 11 個字節。

另一個終端向 /dev/pts/4 發送:

hello asio

由於 echo 會自動在末尾添加換行符 \n,實際發送的是:

hello asio\n

一共 11 個字節。

3.2.完整代碼

#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;
}

運行結果:見下方“運行輸出與時間順序”;如果示例涉及串口、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。

3.3.編譯運行

終端 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

3.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

這個示例的等待時間取決於你什麼時候向串口發送數據。

3.5.本示例需要注意的點

這是同步讀串口。

boost::asio::read() 會阻塞當前線程,直到指定的 buffer 被填滿或者發生錯誤。

本例中:

std::array<char, 11> buffer{};

所以:

boost::asio::read(
    serial,
    boost::asio::buffer(buffer),
    ec);

表示:

一直等待并读取数据
↓
累计收到 11 个字节
↓
read() 返回

如果只發送:

hello

由於還沒有達到 11 個字節,程序仍然會繼續阻塞等待剩餘數據。

因為這裏使用的是同步讀取,所以不需要:

io.run();

3.6.如果實際收到的數據遠遠超過 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

還沒有被程序取走。


3.7.下一次讀取時,會從 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 自動幫你保存每一次讀取的數據。


3.8.如果剩餘數據不夠填滿整個 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 填滿”,而不是“一次串口發送對應一次讀取”。

串口本質上是連續的字節流,並不會自動幫程序保存“這一條消息”和“下一條消息”的邊界。


3.9.關鍵函數説明

3.9.1.serial.open(port_name, ec)

作用:打開串口設備。

參數:

port_name

例如:

/dev/ttyUSB0
/dev/ttyACM0
/dev/pts/3
ec

用於接收錯誤信息。

如果打開失敗:

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

3.9.2.serial.set_option(...)

作用:設置串口參數,例如波特率、數據位、校驗位、停止位和流控。

本示例設置:

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

也就是常見的:

115200 8N1

3.9.3.std::array<char, 11> buffer{}

作用:創建一個 11 字節大小的接收緩衝區。

std::array<char, 11> buffer{};

可以簡單理解成:

准备一块 11 字节的连续内存
用来存放这一次读取到的串口数据

它的容量固定為 11 字節。

如果串口中的數據超過 11 字節:

本次最多读取 11 字节
↓
多出来的数据不会写越界
↓
剩余数据等待下一次读取

如果下一次繼續使用同一個:

buffer

那麼新數據會再次從:

buffer[0]

開始寫,覆蓋上一次的數據。


3.9.4.boost::asio::buffer(buffer)

作用:把 buffer 對應的連續內存包裝成 Asio 可以使用的 buffer。

boost::asio::buffer(buffer)

可以簡單理解成告訴 Asio:

这块内存从哪里开始
+
这块内存有多大

本例中相當於告訴 Asio:

起始地址:buffer.data()

可用空间:11 字节

因此 Asio 不會向這塊內存寫入超過 11 字節的數據。

它本身不會複製數據,也不會擁有這塊內存。


3.9.5.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 被填滿或者發生錯誤。


3.9.6.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] 開始寫並覆蓋舊數據。


4.some()相關的讀取函數(不常用)

4.1.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() 只進行一次讀寫操作,實際處理多少就返回多少。


4.1.1.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 讀滿再返回。


4.1.2.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 填滿。


4.1.3.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 都寫出去再返回。


4.1.4.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()

可以簡單理解成:

這一次能寫多少就先寫多少,然後告訴我實際寫了多少。


4.2.四個 API 對比

API 類型 是否要求整個 buffer 完成
boost::asio::read() 同步讀取 是,默認一直讀到整個 buffer 被填滿或發生錯誤
serial.read_some() 同步讀取 否,讀取到一些數據就可以返回
boost::asio::write() 同步寫入 是,默認一直寫到整個 buffer 寫完或發生錯誤
serial.write_some() 同步寫入 否,這次實際寫多少就返回多少

可以直接記成:

read()
→ 读满

read_some()
→ 读一些
write()
→ 写完

write_some()
→ 写一些

4.3.為什麼 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(...)

4.4.使用時怎麼選擇?

如果你的需求是:

我明确知道需要读取固定长度的数据

例如:

固定读取 8 字节
固定读取 32 字节
固定读取一个已知长度的数据块

可以使用:

boost::asio::read()

如果你的需求是:

串口来了多少数据我先取多少

則更適合:

read_some()

發送也是一樣。

如果你的需求是:

我要把整个 buffer 全部发送出去

通常直接使用:

boost::asio::write()

如果你希望:

自己控制每一次底层写操作

才會直接使用:

write_some()

一句話記住:

read() / write() 是“把整個 buffer 處理完”;read_some() / write_some() 是“這一次能處理多少就先處理多少”。


5.示例 3:普通 main() 裏異步發送串口數據

5.1.程序目標

前面的示例 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() 返回

5.2.完整代碼

#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;
}

5.3.編譯運行

終端 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:編譯運行程序。

5.4.運行輸出與時間順序

程序輸出類似:

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();

5.5.本示例需要注意的點

同步發送:

boost::asio::write(...)

流程是:

调用 write()
↓
当前线程阻塞
↓
整个 buffer 发送完成
↓
write() 返回

異步發送:

boost::asio::async_write(...)

流程是:

调用 async_write()
↓
登记异步发送任务
↓
立即返回
↓
io.run() 驱动任务
↓
发送完成
↓
调用回调

所以異步接口通常必須配合:

io.run();

如果本例刪掉:

io.run();

那麼程序很快就會走到:

return 0;

異步任務將得不到正常的事件循環驅動。


5.6.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

5.7.為什麼 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 指向的内存失效

5.8.boost::asio::async_write() 説明

boost::asio::async_write(
    serial,
    boost::asio::buffer(msg),
    handler);

作用:

異步發送整個指定 buffer。

它不會等待發送完成。

而是:

注册任务
↓
立即返回
↓
io.run() 驱动
↓
整个 buffer 发送完成
↓
执行 handler

一句話記住:

async_write() = “我要異步把整個 buffer 發完,完成以後調用回調”。


6.示例 4:普通 main() 裏異步讀取串口數據

6.1.程序目標

前面的示例 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() 返回

6.2.完整代碼

#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;
}

6.3.編譯運行

終端 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

6.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()
↓
等待读取完成

6.5.本示例需要注意的點

同步讀取:

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()

才會被調用。


6.6.為什麼把 &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()

訪問真正收到的數據。


6.7.buffer 生命週期

這裏:

std::array<char, 11> buffer{};

必須保證在:

async_read()

完成之前一直存在。

本例中:

buffer 创建于 main()
↓
注册 async_read()
↓
io.run()
↓
读取完成
↓
on_read()
↓
io.run() 返回
↓
main() 最后结束

所以 buffer 的生命週期足夠長。

這一點和異步發送中的:

msg

是同一個問題:

異步操作使用的底層數據,在操作完成以前不能被銷燬。


6.8.boost::asio::async_read() 説明

boost::asio::async_read(
    serial,
    boost::asio::buffer(buffer),
    handler);

作用:

異步讀取,直到指定的整個 buffer 被填滿或者發生錯誤。

它本身立即返回。

真正的讀取工作由:

io.run();

驅動。

完成以後調用:

handler(ec, bytes_transferred);

一句話記住:

async_read() = “我要異步把整個 buffer 讀滿,完成以後調用回調”。


7.示例 5:類裏封裝異步串口寫隊列

7.1.程序目標

前面的示例 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()
正在操作串口

7.2.完整代碼

#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;
}

7.3.編譯運行

終端 1:

socat -d -d \
  PTY,raw,echo=0,link=/tmp/ttyV0 \
  PTY,raw,echo=0,link=/tmp/ttyV1

終端 2:監聽串口另一端。

cat /dev/pts/4

再編譯執行程序

7.4.運行輸出與時間順序

程序輸出類似:

SerialWriter:串口打开成功
加入发送队列:message one
加入发送队列:message two
加入发送队列:message three
main:开始 io.run()

发送完成,字节数 = 12
发送完成,字节数 = 12
发送完成,字节数 = 14

main:发送队列处理完成,程序结束

監聽終端:

message one
message two
message three

7.5.第一次調用 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

7.6.第二次和第三次調用時發生什麼?

第二次:

writer.async_send("message two\n");

此時隊列已經是:

[ message one ]

因此:

writing = true

然後追加:

[ message one ]
[ message two ]

但不會再次調用:

do_write();

因為:

message one

已經在異步發送。

第三次也是一樣:

[ message one   ] ← 正在发送
[ message two   ] ← 等待
[ message three ] ← 等待

7.7.第一條發送完成後發生什麼?

第一條:

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() 返回

7.8.為什麼必須保存發送數據?

這裏不能寫成:

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 生命週期是安全的。


7.9.為什麼異步寫串口通常需要隊列?

如果程序連續產生很多數據:

数据 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() 負責“發送完成後刪除隊首並啓動下一條”。


8.示例 6:類裏封裝連續異步串口讀取

8.1.程序目標

前面的示例 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 字节
↓
...

8.2.完整代碼

#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;
}

8.3.編譯運行

終端 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 字節。

8.4.運行輸出與時間順序

程序可能輸出:

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();

一直有尚未完成的異步讀取任務需要等待。


8.5.連續異步讀取是怎麼形成的?

程序剛創建對象:

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() 返回
↓
程序结束

8.6.為什麼 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
状态
队列

都保存成成員變量。


8.7.為什麼下一次讀取會覆蓋上一次的 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() 中处理完成
↓
才重新启动下一次读取

8.8.為什麼這裏不會出現兩個讀取同時修改 buffer_?

因為:

start_read();

不是不停地連續調用很多次。

而是:

第一次 async_read()
↓
等待完成
↓
on_read()
↓
当前读取已经结束
↓
才 start_read()
↓
注册下一次 async_read()

所以任何時刻只有:

一个 async_read()

正在使用:

buffer_

流程嚴格是:

读取 1
↓
完成

读取 2
↓
完成

读取 3
↓
完成

而不是:

读取 1 ────────┐
读取 2 ────────┼→ 同时操作 buffer_
读取 3 ────────┘

8.9.成員函數的 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);

8.10.示例 4 和示例 6 的區別

示例 4:

注册一次 async_read()
↓
读取一次
↓
回调一次
↓
结束

示例 6:

start_read()
↓
async_read()
↓
on_read()
↓
start_read()
↓
async_read()
↓
on_read()
↓
...

所以:

示例 4
→ 学习一次异步读取是怎么工作的

示例 6
→ 学习怎样把异步读取封装进类并持续运行

一句話記住:

連續異步讀取的核心不是寫一個死循環,而是在一次異步讀取完成的回調裏,再註冊下一次異步讀取。


9.示例 7:類裏同時封裝異步串口發送和接收

9.1.程序目標

前面的兩個示例分別完成了:

示例 5:
异步发送队列

示例 6:
连续异步接收

這一節把它們合併到同一個類中。

最終得到:

AsyncSerialPort
│
├── 接收
│   │
│   ├── start_read()
│   ├── async_read()
│   └── on_read()
│        ↓
│      再次 start_read()
│
└── 发送
    │
    ├── async_send()
    ├── write_queue_
    ├── do_write()
    └── on_write()
         ↓
       继续发送队首

這個類可以:

一边持续接收串口数据

同时

一边异步发送串口数据

本示例仍然只有:

一个线程调用 io.run()

所以暫時不涉及多線程併發問題。

9.2.完整代碼

#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;
}

9.3.編譯運行

終端 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 字節。

9.4.整個類的工作過程

程序創建:

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()          发送下一条

接收和發送可以同時存在。

也就是説:

一个异步读操作
+
一个异步写操作

可以同時掛在同一個串口對象上。


9.5.為什麼這裏不需要 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

9.6.本示例最重要的結構

可以把整個類記成:

接收:

start_read()
↓
async_read()
↓
on_read()
↓
处理数据
↓
start_read()

發送:

async_send()
↓
加入 write_queue_
↓
do_write()
↓
async_write()
↓
on_write()
↓
pop_front()
↓
继续下一条

一句話記住:

一個完整的異步串口類,通常同時包含“持續讀取鏈”和“發送隊列鏈”。


10.示例 8:主線程產生數據,獨立 Asio 線程負責串口通信

這種情況一般適用於一個串口設備。(此時main有1個主線程,asio有1個線程)

10.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 線程負責“真正處理串口”。

10.2.完整代碼

#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;
}

10.3.編譯運行

終端 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

10.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线程

去執行。


10.5.為什麼要使用 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

10.6.如果直接共享 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

最典型的用途:

多個線程必須直接訪問同一個共享可變數據時,用鎖保護臨界區。


10.7.但本例為什麼沒有使用 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

這是一個非常重要的併發設計思想:

能通過“讓某個狀態只屬於一個線程”解決的問題,就不一定非要讓多個線程共享它再到處加鎖。


10.8.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 线程提交任务

10.9.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 線程暫時沒任務時也不要退出,後面其他線程還可能繼續投遞任務。


10.10.為什麼結束時先 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() 返回
↓
线程结束

10.11.示例 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 的需求。


11.示例 9:多個線程同時運行 io.run(),使用 strand 避免 handler 併發

這種情況一般適用於多個通信設備。(此時main有1個主線程,asio有2+個線程)

11.1.程序目標

示例 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 不會彼此併發執行。

11.2.完整代碼

#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;
}

11.3.編譯運行

終端 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

11.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_

於是產生數據競爭。


11.5.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()          ]
            ↑
          重叠执行

11.6.strand 不是把串口固定到某一個線程

這一點非常重要。

可能第一次:

on_read()

在線程 1 執行。

下一次:

on_read()

在線程 2 執行。

例如輸出可能是:

收到数据,线程 id = 111111

发送完成,线程 id = 222222

收到数据,线程 id = 222222

发送完成,线程 id = 111111

這是完全正常的。

strand 保證的是:

执行时间不重叠

而不是:

永远固定在线程1

所以:

strand
≠ 固定线程

strand
= 串行化一组 handler

11.7.為什麼不僅 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_ 的异步状态变化

都處於同一套串行化規則中。


11.8.那這裏能不能不用 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

11.9.示例 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 不會彼此併發執行。

12.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。

所以幾乎就是同一代,幾乎沒有任何差距。

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