第 23.1.2 节

Boost.Asio 基础

作者
董佳辉的头像董佳辉
发布于 2023/10/05更新于 2026/10/06

本节不急着讲串口、TCP、UDP,而是把 io_context、run()、post()、work_guard、buffer、std::bind、对象生命周期这些基础讲清楚。
这些东西如果没搞懂,后面串口和网络程序会出现“为什么回调不执行”“为什么程序直接退出”“为什么段错误”等问题。


示例 1:io_context 没有任务时,run() 立刻返回

程序目标

验证一个非常重要的现象:如果 io_context 里没有任何未完成任务,io.run() 会立刻返回。

完整代码

#include <boost/asio.hpp>
#include <print>

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    // io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
    boost::asio::io_context io;

    std::println("main:准备调用 io.run()");

    // 启动事件循环,前面注册的异步任务会在这里被调度执行。
    std::size_t count = io.run();

    std::println("main:io.run() 返回,执行了 {} 个回调", count);

    return 0;
}

运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。

运行输出与时间顺序

这个程序不会等待,立刻输出:

main:准备调用 io.run()
main:io.run() 返回,执行了 0 个回调

本示例需要注意的点

io.run() 不是永远阻塞。它阻塞的前提是:

io_context 里还有未完成的异步任务,或者还有 work_guard 保持它不退出。

如果什么任务都没有,它会马上返回。

io.run() 返回值

io.run() 返回值表示执行了多少个 handler。

本例中没有任何任务,所以返回值是:

0

示例 2:普通 main() 里用 post() 投递任务

程序目标

boost::asio::post() 可以把一个普通函数投递到 io_context 里,让它以后由 io.run() 执行。

完整代码

#include <boost/asio.hpp>
#include <functional>
#include <print>
#include <string>

void print_msg(const std::string& msg)
{
    std::println("执行任务:{}", msg);
}

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    // io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
    boost::asio::io_context io;

    std::println("main:投递任务 A");
    // post 只是把任务放进队列,真正执行要等 io.run()。
    boost::asio::post(io, std::bind(print_msg, std::string("A")));

    std::println("main:投递任务 B");
    boost::asio::post(io, std::bind(print_msg, std::string("B")));

    std::println("main:准备调用 io.run()");

    // 启动事件循环,前面注册的异步任务会在这里被调度执行。
    std::size_t count = io.run();

    std::println("main:io.run() 返回,执行了 {} 个任务", count);

    return 0;
}

运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。

运行输出与时间顺序

程序立刻输出:

main:投递任务 A
main:投递任务 B
main:准备调用 io.run()
执行任务:A
执行任务:B
main:io.run() 返回,执行了 2 个任务

本示例需要注意的点

post() 不是立刻执行函数,而是把函数放进 io_context 的任务队列。

真正执行发生在:

io.run();

post() 函数说明

boost::asio::post(io, handler);

参数:

  1. io:任务投递到哪个 io_context;

  2. handler:将来要执行的函数对象。

返回值:通常不用关心。

std::bind 说明

std::bind(print_msg, std::string("A"))

表示生成一个“无参数函数对象”。以后执行它时,相当于执行:

print_msg("A");

示例 3:work_guard 让 io.run() 等待未来任务

程序目标

首先要理解 io.run() 一个非常重要的规则:

io.run() 不是一个永远运行的死循环。

当 io_context 发现:

现在没有 handler 可以执行

并且

以后也没有已经登记的异步工作需要等待

那么:

io.run();

就会直接返回。

例如:

boost::asio::io_context io;

io.run();

这里什么任务都没有,所以:

进入 run()
↓
发现没有任何工作
↓
立即返回

这对于机器人通信线程就可能产生一个问题。

假设程序启动时先创建 Asio 线程:

通信线程
↓
io.run()

但是串口、定时器或者其他任务要等一会儿才从别的线程提交。

那么可能发生:

Asio 线程启动
↓
io.run()
↓
此时任务队列为空
↓
run() 认为已经没事可做
↓
直接返回
↓
Asio 通信线程结束

过了 2 秒
↓
其他线程才 post() 一个任务
↓
已经没有线程在 run() 了
↓
这个任务没人执行

work_guard 就是为了解决这种情况。

可以先把它理解成:

给 io_context 人为登记一份“还有工作没有完成”的占位工作。

只要这个 guard 还有效:

io_context:
虽然现在任务队列是空的,

但还有一份 outstanding work,

所以我不能认为工作已经全部结束。

于是:

io.run();

不会因为“暂时没任务”而返回,而是继续阻塞等待以后可能到来的任务。


如果没有 work_guard

先看这个过程:

boost::asio::io_context io;

io.run();

此时:

任务数量 = 0
未完成异步操作 = 0
work_guard = 0

所以:

io.run()
↓
没有任何工作
↓
直接返回

可以把 run() 粗略理解成:

while (还有工作需要处理)
{
    等待事件
    执行 handler
}

return;

注意这里的:

还有工作

不只是指:

当前任务队列里已经有 handler

也包括:

已经启动但还没有完成的异步操作

例如:

timer.async_wait(...);

虽然 timer 还要 2 秒才到期,但 Asio 已经知道:

这里还有一个异步操作没完成

所以这种情况下:

io.run();

本来就不会立刻返回。

这也是为什么用 timer.async_wait() 来演示 work_guard 并不好:

timer 自己已经能够让 io.run() 保持运行,反而看不出 guard 的作用。


work_guard 到底做了什么

创建:

auto guard = boost::asio::make_work_guard(io);

以后,可以粗略理解成:

io_context 内部的“未完成工作数量”
额外 +1

注意,这里的 +1 只是帮助理解,并不代表内部真的就是一个简单整数。

可以想象成:

创建 io_context

未完成工作:
0

创建 guard:

make_work_guard(io)

未完成工作:
0 + 一份 guard 占位工作

于是即使现在:

没有 timer

没有 socket

没有 post 的 handler

没有任何真正任务

io.run() 看到 guard 还持有 outstanding work,也不会退出。

它会:

没有任务
↓
但是 guard 还在
↓
不能返回
↓
阻塞等待新任务

注意:

work_guard 不会让 io.run() 疯狂空转。

它不是:

while (true)
{
}

没有任务时,io.run() 仍然可以阻塞等待,不会因为 guard 就白白占满一个 CPU 核心。


完整代码

这个例子真正模拟:

Asio 通信线程已经启动

但当前没有任何任务

2 秒以后另一个线程才提交任务
#include <boost/asio.hpp>
#include <chrono>
#include <functional>
#include <print>
#include <thread>

using namespace std::chrono_literals;

using WorkGuard =
    boost::asio::executor_work_guard<
        boost::asio::io_context::executor_type>;

void delayed_post(
    boost::asio::io_context* io,
    WorkGuard* guard)
{
    std::println("工作线程:等待 2 秒再提交任务");

    std::this_thread::sleep_for(2s);

    std::println("工作线程:向 io_context 提交任务");

    boost::asio::post(
        *io,
        [] {
            std::println(
                "Asio handler:收到后来提交的任务");
        });

    std::println("工作线程:释放 work_guard");

    guard->reset();
}

int main()
{
    boost::asio::io_context io;

    WorkGuard guard =
        boost::asio::make_work_guard(io);

    std::thread worker(
        std::bind(
            delayed_post,
            &io,
            &guard));

    std::println("main:开始 io.run()");

    std::size_t count = io.run();

    std::println(
        "main:io.run() 返回,执行了 {} 个 handler",
        count);

    worker.join();

    return 0;
}

运行结果大致为:

main:开始 io.run()
工作线程:等待 2 秒再提交任务

工作线程:向 io_context 提交任务
工作线程:释放 work_guard
Asio handler:收到后来提交的任务

main:io.run() 返回,执行了 1 个 handler

具体打印先后可能因为线程调度略有变化,但核心流程不变。


整个执行过程

程序首先创建:

boost::asio::io_context io;

此时:

io_context
↓
没有任何任务

然后:

WorkGuard guard =
    boost::asio::make_work_guard(io);

相当于告诉 io_context:

虽然现在可能没有任务,

但先不要认为整个事件循环已经结束,

以后还可能有人给你提交工作。

接着启动:

std::thread worker(...);

这个线程会先:

std::this_thread::sleep_for(2s);

所以刚开始的时候,io_context 里面其实真的没有任何 handler 可以执行。

此时 main 执行:

io.run();

如果没有 guard,逻辑会变成:

io.run()
↓
当前没有任何工作
↓
立即返回

但现在有 guard:

io.run()
↓
当前没有 handler
↓
但是 work_guard 仍然有效
↓
说明仍然存在 outstanding work
↓
不能退出
↓
继续阻塞等待

于是 main 就停在:

io.run();

里面等待。


2 秒以后发生什么

worker 睡眠结束:

std::this_thread::sleep_for(2s);

然后执行:

boost::asio::post(
    *io,
    [] {
        std::println(
            "Asio handler:收到后来提交的任务");
    });

post() 相当于:

向 io_context 投递一个以后要执行的 handler。

于是原本正在:

阻塞等待

的 io.run() 发现:

有新任务来了

然后执行这个 handler。


guard.reset() 到底做什么

接下来:

guard->reset();

它并不是:

停止 io_context

也不是:

取消所有任务

更不是:

让 run() 立刻强制返回

它真正表达的是:

我之前登记的那份“占位工作”现在结束了,你以后不用再为了我强行保持 run() 存活。

可以粗略理解成:

创建 guard

outstanding work:
+1

然后:

guard.reset()

outstanding work:
-1

于是 io_context 又恢复正常退出规则:

如果还有真正的任务
→ 继续处理

如果还有未完成异步操作
→ 继续等待

如果什么都没有了
→ run() 可以返回

所以:

guard.reset();

不是:

命令 run() 退出

而是:

取消“禁止 run() 因没工作而退出”这个限制

这一点非常重要。


为什么 post 以后再 reset

本例中:

boost::asio::post(*io, handler);

guard->reset();

顺序可以理解成:

先把真正的工作提交进去
↓
然后取消 guard 的占位工作

这时 io_context 中仍然还有:

刚刚 post 的 handler

所以即使 guard 被 reset:

还有真正工作
↓
run() 继续
↓
执行 handler
↓
handler 执行结束
↓
此时已经没有其他工作
↓
run() 返回

完整过程:

创建 io_context
│
├─ 当前没有任务
│
↓
创建 work_guard
│
├─ 人为保持 outstanding work
│
↓
main:io.run()
│
├─ 当前没 handler
│
├─ 但 guard 还在
│
└─ 所以阻塞等待,不退出
│
│
│       worker 睡眠 2 秒
│
│               ↓
│       post(handler)
│               ↓
│       guard.reset()
│
↓
io.run() 收到 handler
│
↓
执行 handler
│
↓
handler 完成
│
├─ guard 已 reset
├─ 没有其他任务
├─ 没有未完成异步操作
│
↓
io.run() 返回

这就是 work_guard 的完整作用。


如果一直不 reset() 会怎样

假设:

WorkGuard guard =
    boost::asio::make_work_guard(io);

io.run();

又永远不:

guard.reset();

那么即使所有真正任务都已经执行完:

handler 全执行完了

timer 全结束了

socket 也没任务了

io.run() 仍然会认为:

还有一份 outstanding work

所以:

不会正常返回

它会继续等待以后可能到来的任务。

这正是后台 Asio 线程经常需要的效果。


reset() 和 io.stop() 不一样

这两个很容易混。

guard.reset() 的意思是:

允许 io.run() 在所有真正工作完成以后自然退出。

也就是:

reset()
↓
不再人为保持 io_context 存活
↓
还有任务就继续执行
↓
任务全部完成后 run() 自然返回

而:

io.stop();

更像:

要求这个 io_context 尽快停止运行。

所以简单理解:

guard.reset()

→ 我不再拦着你退出
→ 剩余工作正常处理
→ 没工作以后自然退出

而:

io.stop()

→ 主动要求事件循环停止

因此如果希望:

尽量优雅地把已经提交的任务处理完再退出

通常更关注:

guard.reset();

如果希望主动让事件循环停止,则可以使用:

io.stop();

机器人程序中为什么经常需要 work_guard

例如你的程序可能是:

main
│
├─ 创建 io_context
│
├─ 创建串口驱动
│
├─ 创建 Asio 通信线程
│
└─ ROS2 开始运行

Asio 通信线程:

io.run();

而真正的异步任务可能是之后才出现:

ROS2 callback
↓
要求发送串口命令
↓
post 到 io_context

或者

设备稍后连接
↓
启动 async_read

或者

以后动态创建 timer

如果 Asio 线程刚启动时:

暂时没有任何 outstanding work

那么没有 guard:

Asio 线程
↓
io.run()
↓
没任务
↓
直接返回
↓
通信线程死掉

使用 guard:

Asio 线程
↓
io.run()
↓
暂时没任务
↓
guard 还在
↓
继续等待
↓
以后任务来了
↓
正常处理

因此可以把 work_guard 理解成:

给长期运行的 Asio 事件循环办理了一张“先别下班”的凭证。


make_work_guard() 和 reset() 怎么记

创建:

auto guard =
    boost::asio::make_work_guard(io);

可以理解成:

告诉 io_context:

“我这里还有未来工作,
即使眼前没任务,你也先别让 run() 下班。”

释放:

guard.reset();

可以理解成:

告诉 io_context:

“我这边以后不会再依赖你继续保持运行了,
如果其他工作也都结束了,你可以让 run() 返回。”

所以它们不是:

make_work_guard()
→ 创建一个实际任务

reset()
→ 删除一个实际任务

而是:

make_work_guard()
→ 持有一份 outstanding work

reset()
→ 放弃这份 outstanding work

本示例最重要的结论

没有 work_guard:

io.run()
↓
暂时没有工作
↓
直接返回

有 work_guard:

io.run()
↓
暂时没有工作
↓
但 guard 仍持有 outstanding work
↓
继续阻塞等待未来任务

释放 guard:

guard.reset()
↓
不再人为维持 io_context 存活
↓
如果真正工作也全部结束
↓
io.run() 返回

所以一句话记住:

work_guard 不是一个真正要执行的任务,而是一份“还有工作存在”的占位声明,用来防止 io.run() 在暂时无任务时提前返回。


示例 4:buffer 的基本使用

程序目标

Boost.Asio 读写数据时经常写:

boost::asio::buffer(data)

本例先看它的基本含义。

完整代码

#include <boost/asio.hpp>
#include <array>
#include <print>
#include <string>

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    std::string msg = "hello asio";
    auto buf1 = boost::asio::buffer(msg);

    // std::array 是固定长度数组,长度在编译期就确定。
    std::array<char, 128> data;
    auto buf2 = boost::asio::buffer(data);

    std::println("msg.size() = {}", msg.size());
    std::println("buffer(msg).size() = {}", buf1.size());
    std::println("array size = {}", data.size());
    std::println("buffer(data).size() = {}", buf2.size());

    return 0;
}

运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。

运行输出

msg.size() = 10
buffer(msg).size() = 10
array size = 128
buffer(data).size() = 128

本示例需要注意的点

boost::asio::buffer() 通常不会复制数据,它只是生成一个“指向已有内存的 buffer 视图”。

所以异步读写时特别注意:

buffer 指向的原始数据必须活到异步回调完成。

错误写法示意:

void send()
{
    std::string msg = "hello";
    boost::asio::async_write(socket_, boost::asio::buffer(msg), handler);
}

这个 msg 在 send() 返回后就销毁了,但异步写可能还没完成。

正确思路:

把 msg 做成类成员变量
或者用 shared_ptr 管理
或者使用写队列保存待发送数据

boost::asio::buffer() 可以接收哪些类型

boost::asio::buffer(data) 的作用可以简单理解为:

把一段连续内存包装成 Asio 可以使用的 buffer 描述对象。

它本身不会复制数据,主要记录的是:

内存起始地址
+
内存长度

所以最常见的要求是:

data 最好是一段连续存储的内存。

常见可直接使用的类型包括:

char data[128];

std::array<char, 128> data;

std::array<std::uint8_t, 128> data;

std::vector<char> data;

std::vector<std::uint8_t> data;

std::string data;

例如:

std::array<std::uint8_t, 256> data;

auto buffer = boost::asio::buffer(data);

或者:

//创建一个名为 data 的 std::vector,里面有 256 个 std::uint8_t 元素。
std::vector<std::uint8_t> data(256);

auto buffer = boost::asio::buffer(data);

字符串也可以:

std::string message = "hello";

auto buffer = boost::asio::buffer(message);

还可以直接指定:

指针 + 长度

例如:

std::uint8_t data[256];

auto buffer =
    boost::asio::buffer(data, 100);

表示只使用:

data 开始位置后的前 100 字节

也可以:

std::uint8_t* data = ...;
std::size_t size = ...;

auto buffer =
    boost::asio::buffer(data, size);

对于串口、网络等字节流通信,通常优先使用:

std::uint8_t
unsigned char
char
std::byte

例如:

std::array<std::uint8_t, 256> rx_buffer;

这是很常见的接收缓冲区写法。


像下面这些容器不能简单看成一整块连续内存:

std::list
std::deque

因此一般不能像:

std::vector
std::array
std::string

那样直接作为一整块连续 buffer 使用。


还要注意 const。

例如:

const std::string message = "hello";

那么:

boost::asio::buffer(message);

得到的是只读 buffer,适合:

发送数据

但不能作为接收缓冲区,因为接收操作需要修改底层内存。

可以简单理解成:

非 const 数据
→ 可以生成可写 buffer

const 数据
→ 只能生成只读 buffer

最后还有一个非常重要的点:

boost::asio::buffer(data) 不拥有 data 的内存。

例如:

std::string data = "hello";

auto buffer = boost::asio::buffer(data);

这里 buffer 只是指向 data 的内存。

所以异步操作:

boost::asio::async_write(
    socket,
    boost::asio::buffer(data),
    handler);

完成之前,必须保证:

data 仍然存在

并且底层内存没有失效

因此可以把:

boost::asio::buffer(data)

简单记成:

把一段连续内存包装成“地址 + 长度”的 Asio buffer,并不会复制或拥有这段数据。


示例 5:类里封装 io_context 工作线程

程序目标

工程里经常希望 Asio 在单独线程运行。
本例写一个最小的 IoThread 类:

  1. 构造时创建 work_guard;

  2. start() 开线程执行 io.run();

  3. stop() 停止线程;

  4. 用 post() 投递任务验证效果。

完整代码

#include <boost/asio.hpp>
#include <functional>
#include <print>
#include <memory>
#include <string>
#include <thread>

class IoThread
{
public:
    IoThread()
        : guard_(boost::asio::make_work_guard(io_))
    {
    }

    ~IoThread()
    {
        stop();
    }

    void start()
    {
        thread_ = std::thread(std::bind(&IoThread::run, this));
    }

    void stop()
    {
        if (!stopped_)
        {
            stopped_ = true;
            guard_.reset();
            io_.stop();

            if (thread_.joinable())
            {
                thread_.join();
            }
        }
    }

    boost::asio::io_context& io()
    {
        return io_;
    }

private:
    void run()
    {
        std::println("IoThread:io.run() 开始");
        // 启动事件循环,前面注册的异步任务会在这里被调度执行。
        io_.run();
        std::println("IoThread:io.run() 返回");
    }

private:
    // io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
    boost::asio::io_context io_;
    boost::asio::executor_work_guard<boost::asio::io_context::executor_type> guard_;
    std::thread thread_;
    bool stopped_ = false;
};

void print_task(const std::string& msg)
{
    std::println("执行任务:{}", msg);
}

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    IoThread io_thread;
    io_thread.start();

    // post 只是把任务放进队列,真正执行要等 io.run()。
    boost::asio::post(io_thread.io(), std::bind(print_task, std::string("A")));
    boost::asio::post(io_thread.io(), std::bind(print_task, std::string("B")));

    std::this_thread::sleep_for(std::chrono::seconds(1));

    std::println("main:准备停止 IoThread");
    io_thread.stop();

    std::println("main:结束");

    return 0;
}

运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。

运行输出与时间顺序

程序启动后,工作线程开始运行:

IoThread:io.run() 开始
执行任务:A
执行任务:B

约 1 秒后主线程停止它:

main:准备停止 IoThread
IoThread:io.run() 返回
main:结束

完整输出类似:

IoThread:io.run() 开始
执行任务:A
执行任务:B
main:准备停止 IoThread
IoThread:io.run() 返回
main:结束

本示例需要注意的点

这个类就是以后封装串口、TCP、UDP 通信线程的雏形。

但注意:

io_.stop();

会让 io_context 尽快停止,未完成的异步任务可能不会正常完成。

工程里更细致的做法是:

先 cancel socket / timer / serial_port
再 reset work_guard
最后等待 run() 返回

本节总结

  1. 没有任务时,io.run() 立刻返回。

  2. post() 可以把普通函数投递给 io_context 执行。

  3. work_guard 可以防止 io.run() 因为没任务而退出。

  4. std::bind 可以绑定普通函数,也可以绑定成员函数。

  5. boost::asio::buffer() 通常只是内存视图,不负责延长数据生命周期。

  6. 异步工程里最重要的是对象生命周期和 buffer 生命周期。