跳到正文

Wiki

Boost.Asio非同步IO庫

約 3 分鐘閱讀

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

本章面向機器人、ROS2、下位機序列埠、TCP/UDP 通訊學習。
本教程刻意採用 std::bind,暫時不使用 lambda,方便初學者先把“回撥函式、佔位符、成員函式繫結、非同步執行順序”看清楚。


1.本套教程的章節順序

建議學習順序如下:

2100-0100-Boost.Asio异步IO库.md
2100-0100-0100-定时器与异步IO.md
2100-0100-0200-Boost.Asio基础.md
2100-0100-0300-串口通信.md
2100-0100-0400-TCP通信.md
2100-0100-0500-UDP通信.md
2100-0100-0600-机器人工程写法与ROS2集成.md

我把“定時器”提前,是因為定時器不依賴序列埠硬體、不依賴網路對端,最適合看清楚:

  1. io_context.run() 為什麼會阻塞;
  2. async_wait() 為什麼不是立刻執行回撥;
  3. 一個 timer 和兩個 timer 的區別;
  4. 回撥函式到底在哪個執行緒裡執行;
  5. std::bind 裡的 _1this&Class::func 到底什麼意思。

2.本套教程統一約定

2.1.統一使用標準庫寫法

本教程儘量使用標準庫:

#include <chrono>
#include <functional>
#include <memory>
#include <thread>
#include <string>

例如定時器時間統一寫:

std::chrono::seconds(1)
std::chrono::milliseconds(100)

而不是優先寫:

boost::asio::chrono::seconds(1)

2.2.回撥統一使用 std::bind

本教程裡非同步回撥儘量寫成:

timer.async_wait(std::bind(on_timer, std::placeholders::_1));

成員函式回撥寫成:

timer_.async_wait(std::bind(&Printer::on_timer, this, std::placeholders::_1));

讀寫回調有兩個引數時寫成:

socket.async_read_some(
    boost::asio::buffer(data_),
    std::bind(&Session::on_read,
              this,
              std::placeholders::_1,
              std::placeholders::_2));

其中:

std::placeholders::_1
std::placeholders::_2

表示“非同步操作完成時,Boost.Asio 自動傳給回撥函式的第 1 個、第 2 個引數”。

2.3.錯誤碼仍然使用 Boost.Asio 的型別

這個不要亂換:

const boost::system::error_code& ec

原因是 Boost.Asio 的非同步回撥預設把錯誤傳給 boost::system::error_code。以後如果你換 standalone Asio 或者標準網路庫,再考慮對應型別。


3.Boost.Asio 的核心思想

Boost.Asio 可以先粗暴理解成:

io_context = 事件循环 / 调度器
socket / serial_port / timer = IO对象
async_xxx() = 注册一个异步任务
handler = 异步任务完成后执行的回调函数
run() = 开始处理异步任务和回调函数

最小非同步程式大概長這樣:

boost::asio::io_context io;
boost::asio::steady_timer timer(io, std::chrono::seconds(2));

timer.async_wait(std::bind(on_timer, std::placeholders::_1));

io.run();

執行邏輯不是:

async_wait 立刻执行 on_timer

而是:

async_wait 注册任务
io.run() 进入事件循环
等待 2 秒
timer 到期
io.run() 调用 on_timer
没有任务了
io.run() 返回

4.編譯環境

4.1.Ubuntu / Debian

sudo apt update
sudo apt install libboost-all-dev g++ cmake

4.2.Fedora

sudo dnf install boost-devel gcc-c++ cmake

4.3.單檔案編譯命令

很多示例可以直接這樣編譯:

g++ demo.cpp -o demo -std=c++17 -lboost_system -pthread

如果你的 Boost 版本較新,某些 Linux 發行版上也許不需要顯式連結 -lboost_system,但初學階段建議先帶上,減少環境差異。

4.4.推薦 CMake 模板

cmake_minimum_required(VERSION 3.16)
project(asio_demo)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Boost REQUIRED COMPONENTS system)

add_executable(demo demo.cpp)
target_link_libraries(demo PRIVATE Boost::system pthread)

5.為什麼先學定時器

機器人通訊裡最容易出問題的不是“API 會不會呼叫”,而是:

  1. 回撥什麼時候執行;
  2. 回撥在哪個執行緒執行;
  3. 物件什麼時候析構;
  4. buffer 資料什麼時候還能用;
  5. run() 為什麼卡住;
  6. run() 為什麼又會提前返回;
  7. 多執行緒時為什麼會資料競爭。

這些問題都可以先用定時器看懂。定時器看懂之後,序列埠、TCP、UDP 本質上只是“等待的事件不同”:

timer 等待时间到期
serial_port 等待串口可读 / 可写
tcp::socket 等待网络可读 / 可写 / 连接完成
udp::socket 等待收到一个数据报

6.學完這套教程應該達到什麼程度

學完之後,你應該能做到:

  1. 看懂 Boost.Asio 官方 timer / TCP / UDP 教程;
  2. 能用 std::bind 寫普通函式回撥、成員函式回撥;
  3. 能解釋 io_context.run() 的阻塞和返回條件;
  4. 能寫序列埠非同步讀取下位機資料;
  5. 能寫 TCP client / server;
  6. 能寫 UDP sender / receiver / echo server;
  7. 能把 Asio 通訊模組封裝成一個類;
  8. 能把通訊模組接進 ROS2 節點,而不是在 ROS2 回撥裡寫阻塞死迴圈。

7.你現在最需要記住的 5 句話

  1. async_xxx() 只是註冊非同步任務,不是立刻執行回撥。
  2. io_context.run() 才是真正驅動非同步任務執行的地方。
  3. 回撥函式只會在正在執行 io_context.run() 的執行緒裡被呼叫。
  4. 非同步 buffer、socket、timer 物件必須活到回撥執行完。
  5. 類裡繫結成員函式時,寫 std::bind(&Class::func, this, _1, _2)