Skip to content

Wiki

Boost.Asio Basics

About 20 min read

Existing English translations were reused. 2 current source blocks remain in Simplified Chinese, without triggering paid translation.

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


1.Example 1:io_context When there is no task,run() return immediately

1.1.Program Objectives

Verify a very important phenomenon: if io_context has no incomplete tasks, io.run() returns immediately.

1.2.Complete code

#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;
}

Running result : See “Running output and chronological order” below; if the example involves timers, threads, networks, or external devices, the specific timing and order may vary slightly depending on the environment.

1.3.Run output and chronological order

This program will not wait and outputs immediately:

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

1.4.Points to note in this example

io.run() It does not block forever. The precondition for it to block is:

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

If there are no tasks at all, it will return immediately.

1.5.io.run() Return value

io.run() The return value indicates how many handlers were executed.

In this example there are no tasks, so the return value is:

0

2.Example 2: In ordinary main(), use post() to deliver tasks

2.1.Program Objectives

boost::asio::post()You can deliver an ordinary function to io_context so that it will be executed later by io.run().

2.2.Complete code

#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;
}

Running result : See “Running output and chronological order” below; if the example involves timers, threads, networks, or external devices, the specific timing and order may vary slightly depending on the environment.

2.3.Run output and chronological order

The program immediately outputs:

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

2.4.Points to note in this example

post()Rather than executing the function immediately, it places the function into the task queue of io_context.

The actual execution occurs at:

io.run();

2.5.post() Function Description

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

Parameters:

  1. io: which target the task is delivered to io_context;
  2. handler: the function object to be executed in the future.

Return value: usually no need to worry about it.

2.6.std::bind Description

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

Represents the generation of a "parameterless function object". When it is executed later, it is equivalent to executing:

print_msg("A");

3.Example 3:work_guard Let io.run() await future tasks

3.1.Program Objectives

First, it is important to understand io.run() a very important rule:

io.run() is not an infinite loop that runs forever.

When io_context discovered:

现在没有 handler 可以执行

并且

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

Then:

io.run();

it will return directly.

For example:

boost::asio::io_context io;

io.run();

There are no tasks here, so:

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

This may create a problem for the robot communication thread.

Assume that the program creates the Asio thread first at startup:

通信线程
↓
io.run()

However, the serial port, timer, or other tasks must wait a while before being submitted from another thread.

Then it may happen:

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

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

work_guardThis is precisely to address this situation.

You can first understand it as:

Give io_context Manually register a placeholder job for "there is still work not completed".

As long as this guard is still valid:

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

但还有一份 outstanding work,

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

Therefore:

io.run();

It will not return just because there is “temporarily no task,” but will continue to block and wait for tasks that may arrive later.


3.2.If there is no work_guard

First, look at this process:

boost::asio::io_context io;

io.run();

At this point:

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

Therefore:

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

You can roughly understand run() as:

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

return;

Note here:

还有工作

Not only refers to:

当前任务队列里已经有 handler

Also includes:

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

For example:

timer.async_wait(...);

Although the timer will not expire for another 2 seconds, Asio already knows:

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

So in this case:

io.run();

It would not have returned immediately in the first place.

This is also why using timer.async_wait() to demonstrate work_guard is not good:

timer itself is already able to keep io.run() running, so conversely the role of the guard cannot be seen.


3.3.work_guard What exactly was done

Create:

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

From now on, it can be roughly understood as:

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

Note that the +1 here is only to aid understanding and does not mean that internally it is really just a simple integer.

It can be imagined as:

创建 io_context

未完成工作:
0

Create guard:

make_work_guard(io)

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

So even now:

没有 timer

没有 socket

没有 post 的 handler

没有任何真正任务

io.run()It will also not exit when it sees that the guard still holds outstanding work.

It will:

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

Note:

work_guard It won't let io.run() spin idly like crazy.

It is not:

while (true)
{
}

When there are no tasks,io.run() it can still block and wait, and will not wastefully occupy an entire CPU core just because of the guard.


3.4.Complete code

This example truly simulates:

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;
}

The result is roughly as follows:

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

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

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

The exact printing order may vary slightly due to thread scheduling, but the core flow remains unchanged.


3.5.the entire execution process

The program first creates:

boost::asio::io_context io;

At this point:

io_context
↓
没有任何任务

Then:

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

Equivalent to telling io_context:

虽然现在可能没有任务,

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

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

Next, start:

std::thread worker(...);

This thread will first:

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

So at the very beginning,io_context there really wasn't any handler available to execute inside.

At this point, main executes:

io.run();

Without a guard, the logic would become:

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

But now there is a guard:

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

Thus main stops at:

io.run();

Wait inside.


3.6.What happens after 2 seconds?

worker sleep ended:

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

Then execute:

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

post() Equivalent to:

Post a handler to io_context to be executed later.

Thus originally was in the process of:

阻塞等待

of io.run() Discovery:

有新任务来了

Then execute this handler.


3.7.guard.reset() What exactly to do

Next:

guard->reset();

It is not:

停止 io_context

Nor is it:

取消所有任务

Even less so:

让 run() 立刻强制返回

What it truly expresses is:

The "placeholder job" I registered before has now ended, so you no longer need to force yourself to keep run() alive for my sake.

It can be roughly understood as:

创建 guard

outstanding work:
+1

Then:

guard.reset()

outstanding work:
-1

Then io_context returns to the normal exit rule:

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

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

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

Therefore:

guard.reset();

No:

命令 run() 退出

Instead:

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

This point is very important.


3.8.Why reset after post?

In this example:

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

guard->reset();

The order can be understood as:

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

At this time io_context still remains in:

刚刚 post 的 handler

So even if the guard is reset:

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

Full process:

创建 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() 返回

This is the complete function of work_guard.


3.9.如果一直不 reset() 会怎样

Assumption:

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

io.run();

And never:

guard.reset();

Then even if all real tasks have already been executed:

handler 全执行完了

timer 全结束了

socket 也没任务了

io.run() will still think:

还有一份 outstanding work

Therefore:

不会正常返回

It will continue waiting for tasks that may arrive later.

This is exactly the effect that the background Asio thread often needs.


3.10.reset() and io.stop() differ

These two are easily confused.

guard.reset() means:

Allow io.run() to exit naturally after all real work is complete.

That is:

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

And:

io.stop();

More like:

Required that this io_context stop running as soon as possible.

So in simple terms:

guard.reset()

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

And:

io.stop()

→ 主动要求事件循环停止

Therefore, if it is desired:

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

Usually pay more attention to:

guard.reset();

If you want to actively stop the event loop, you can use:

io.stop();

3.11.Why is work_guard often needed in robot programs?

For example, your program might be:

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

Asio communication thread:

io.run();

However, the actual asynchronous tasks may only appear later:

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

或者

设备稍后连接
↓
启动 async_read

或者

以后动态创建 timer

If the Asio thread has just started:

暂时没有任何 outstanding work

So without a guard:

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

Use guard:

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

Therefore, work_guard can be understood as:

Obtained a 'do not clock out yet' permit for the long-running Asio event loop.


3.12.make_work_guard() and reset() how to remember

Create:

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

It can be understood as:

告诉 io_context:

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

Release:

guard.reset();

It can be understood as:

告诉 io_context:

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

So they are not:

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

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

Instead:

make_work_guard()
→ 持有一份 outstanding work

reset()
→ 放弃这份 outstanding work

3.13.The most important conclusion of this example

No work_guard:

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

there are work_guard:

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

Release guard:

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

So remember this in one sentence:

work_guard is not a task that is actually meant to be executed, but rather a placeholder declaration that "there is still work to do," used to prevent io.run() from returning early when there are temporarily no tasks.


4.Example 4:bufferBasic usage of

4.1.Program Objectives

Boost.Asio When reading and writing data, it is often written as:

boost::asio::buffer(data)

In this example, let's first look at its basic meaning.

4.2.Complete code

#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;
}

Running result : See “Running output and chronological order” below; if the example involves timers, threads, networks, or external devices, the specific timing and order may vary slightly depending on the environment.

4.3.Run output

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

4.4.Points to note in this example

boost::asio::buffer() It typically does not copy data; it merely generates a "buffer view pointing to existing memory".

Therefore, special attention should be paid during asynchronous read and write:

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

Example of incorrect usage:

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

This msg is send() destroyed after returning, but the asynchronous write may not yet have completed.

Correct approach:

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

4.5.boost::asio::buffer()What types can be received

boost::asio::buffer(data)The role of can be simply understood as:

Wrap a contiguous block of memory into a buffer descriptor object that Asio can use.

it itselfdoes not copy dataand primarily records:

内存起始地址
+
内存长度

So the most common requirement is:

data Preferably a contiguous piece of memory.

Common types that can be used directly include:

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;

For example:

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

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

Or:

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

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

Strings can also be used:

std::string message = "hello";

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

You can also specify directly:

指针 + 长度

For example:

std::uint8_t data[256];

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

Indicates that only the following is used:

data 开始位置后的前 100 字节

Alternatively:

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

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

For serial port, network, and other byte stream communications, it is usually preferable to use:

std::uint8_t
unsigned char
char
std::byte

For example:

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

This is a very common way of writing a receive buffer.


Containers like the following cannot simply be viewed as a single contiguous block of memory:

std::list
std::deque

Therefore, in general, it cannot be like:

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

in that way, directly as a single continuous buffer.


Also note const.

For example:

const std::string message = "hello";

Then:

boost::asio::buffer(message);

The result is a read-only buffer, suitable for:

发送数据

However, it cannot be used as a receive buffer because receive operations need to modify the underlying memory.

It can be simply understood as:

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

const 数据
→ 只能生成只读 buffer

Finally, there is one more very important point:

boost::asio::buffer(data) does not own data's memory.

For example:

std::string data = "hello";

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

Here buffer simply points to the memory of data .

So the asynchronous operation:

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

Before completion, the following must be ensured:

data 仍然存在

并且底层内存没有失效

Therefore, we can take:

boost::asio::buffer(data)

Simply remember it as:

Wrapping a contiguous block of memory as an Asio buffer of "address + length" does not copy or own that data.


5.Example 5: Encapsulating in a Class io_context Worker Thread

5.1.Program Objectives

In projects, it is often desirable to run Asio in a separate thread.
This example writes a minimal IoThread class:

  1. Create on construction work_guard;
  2. start()Start thread execution io.run();
  3. stop()Stop thread;
  4. Use post() to submit tasks and verify the effect.

5.2.Complete code

#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;
}

Running result : See “Running output and chronological order” below; if the example involves timers, threads, networks, or external devices, the specific timing and order may vary slightly depending on the environment.

5.3.Run output and chronological order

After the program starts, the worker thread begins running:

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

The main thread stops it after about 1 second:

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

Full output similar to:

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

5.4.Points to note in this example

This class is the prototype for the serial port, TCP, and UDP communication threads to be encapsulated later.

But note:

io_.stop();

will cause io_context to stop as soon as possible; unfinished asynchronous tasks may not complete properly.

A more detailed practice in engineering is:

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

6.Summary of this section

  1. When there are no tasks,io.run() return immediately.
  2. post() Ordinary functions can be submitted to io_context for execution.
  3. work_guard Can prevent io.run() from exiting because there are no tasks.
  4. std::bind Can bind ordinary functions, and can also bind member functions.
  5. boost::asio::buffer() Usually just a memory view, not responsible for extending the data lifetime.
  6. In asynchronous engineering, the most important things are object lifetime and buffer lifetime.