第 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

我把“定时器”提前,是因为定时器不依赖串口硬件、不依赖网络对端,最适合看清楚:

  1. io_context.run() 为什么会阻塞;
  2. async_wait() 为什么不是立刻执行回调;
  3. 一个 timer 和两个 timer 的区别;
  4. 回调函数到底在哪个线程里执行;
  5. std::bind 里的 _1this&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 会不会调用”,而是:

  1. 回调什么时候执行;
  2. 回调在哪个线程执行;
  3. 对象什么时候析构;
  4. buffer 数据什么时候还能用;
  5. run() 为什么卡住;
  6. run() 为什么又会提前返回;
  7. 多线程时为什么会数据竞争。

这些问题都可以先用定时器看懂。定时器看懂之后,串口、TCP、UDP 本质上只是“等待的事件不同”:

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

学完这套教程应该达到什么程度

学完之后,你应该能做到:

  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 回调里写阻塞死循环。

你现在最需要记住的 5 句话

  1. async_xxx() 只是注册异步任务,不是立刻执行回调。
  2. io_context.run() 才是真正驱动异步任务执行的地方。
  3. 回调函数只会在正在执行 io_context.run() 的线程里被调用。
  4. 异步 buffer、socket、timer 对象必须活到回调执行完。
  5. 类里绑定成员函数时,写 std::bind(&Class::func, this, _1, _2)
音乐页