第 19.1 節
Boost.Asio異步IO庫
0瀏覽次數0訪問次數--跳出率--平均停留
本章面向機器人、ROS2、下位機串口、TCP/UDP 通信學習。
本教程刻意採用std::bind,暫時不使用 lambda,方便初學者先把“回調函數、佔位符、成員函數綁定、異步執行順序”看清楚。
本套教程的章節順序
建議學習順序如下:
ch19-1-Boost.Asio异步IO库.md
ch19-1-1-定时器与异步IO.md
ch19-1-2-Boost.Asio基础.md
ch19-1-3-串口通信.md
ch19-1-4-TCP通信.md
ch19-1-5-UDP通信.md
ch19-1-6-机器人工程写法与ROS2集成.md
我把“定時器”提前,是因爲定時器不依賴串口硬件、不依賴網絡對端,最適合看清楚:
io_context.run()爲什麼會阻塞;async_wait()爲什麼不是立刻執行回調;- 一個 timer 和兩個 timer 的區別;
- 回調函數到底在哪個線程裏執行;
std::bind裏的_1、this、&Class::func到底什麼意思。
本套教程統一約定
統一使用標準庫寫法
本教程儘量使用標準庫:
#include <chrono>
#include <functional>
#include <memory>
#include <thread>
#include <string>
例如定時器時間統一寫:
std::chrono::seconds(1)
std::chrono::milliseconds(100)
而不是優先寫:
boost::asio::chrono::seconds(1)
回調統一使用 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 個參數”。
錯誤碼仍然使用 Boost.Asio 的類型
這個不要亂換:
const boost::system::error_code& ec
原因是 Boost.Asio 的異步回調默認把錯誤傳給 boost::system::error_code。以後如果你換 standalone Asio 或者標準網絡庫,再考慮對應類型。
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() 返回
編譯環境
Ubuntu / Debian
sudo apt update
sudo apt install libboost-all-dev g++ cmake
Fedora
sudo dnf install boost-devel gcc-c++ cmake
單文件編譯命令
很多示例可以直接這樣編譯:
g++ demo.cpp -o demo -std=c++17 -lboost_system -pthread
如果你的 Boost 版本較新,某些 Linux 發行版上也許不需要顯式鏈接 -lboost_system,但初學階段建議先帶上,減少環境差異。
推薦 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)
爲什麼先學定時器
機器人通信裏最容易出問題的不是“API 會不會調用”,而是:
- 回調什麼時候執行;
- 回調在哪個線程執行;
- 對象什麼時候析構;
- buffer 數據什麼時候還能用;
run()爲什麼卡住;run()爲什麼又會提前返回;- 多線程時爲什麼會數據競爭。
這些問題都可以先用定時器看懂。定時器看懂之後,串口、TCP、UDP 本質上只是“等待的事件不同”:
timer 等待时间到期
serial_port 等待串口可读 / 可写
tcp::socket 等待网络可读 / 可写 / 连接完成
udp::socket 等待收到一个数据报
學完這套教程應該達到什麼程度
學完之後,你應該能做到:
- 看懂 Boost.Asio 官方 timer / TCP / UDP 教程;
- 能用
std::bind寫普通函數回調、成員函數回調; - 能解釋
io_context.run()的阻塞和返回條件; - 能寫串口異步讀取下位機數據;
- 能寫 TCP client / server;
- 能寫 UDP sender / receiver / echo server;
- 能把 Asio 通信模塊封裝成一個類;
- 能把通信模塊接進 ROS2 節點,而不是在 ROS2 回調裏寫阻塞死循環。
你現在最需要記住的 5 句話
async_xxx()只是註冊異步任務,不是立刻執行回調。io_context.run()纔是真正驅動異步任務執行的地方。- 回調函數只會在正在執行
io_context.run()的線程裏被調用。 - 異步 buffer、socket、timer 對象必須活到回調執行完。
- 類裏綁定成員函數時,寫
std::bind(&Class::func, this, _1, _2)。