Wiki
序列埠通訊教學
本文由簡體中文內容確定性轉換,並受版本化術語表保護。
序列埠是機器人裡最常見的通訊方式之一:上位機 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。
所以幾乎就是同一代,幾乎沒有任何差距。
其他的全部可以原封不動保留。