跳到正文

Wiki

Boost.Asio 基礎

約 6 分鐘閱讀

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

本節不急著講序列埠、TCP、UDP,而是把 io_contextrun()post()work_guardbufferstd::bind、物件生命週期這些基礎講清楚。
這些東西如果沒搞懂,後面序列埠和網路程式會出現“為什麼回撥不執行”“為什麼程式直接退出”“為什麼段錯誤”等問題。


1.示例 1:io_context 沒有任務時,run() 立刻返回

1.1.程式目標

驗證一個非常重要的現象:如果 io_context 裡沒有任何未完成任務,io.run() 會立刻返回。

1.2.完整程式碼

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

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

    std::cout << "main:准备调用 io.run()" << std::endl;

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

    std::cout << "main:io.run() 返回,执行了 " << count << " 个回调" << std::endl;

    return 0;
}

執行結果:見下方“執行輸出與時間順序”;如果示例涉及定時器、執行緒、網路或外部裝置,具體時間和順序可能會隨環境略有變化。

1.3.編譯執行

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

1.4.執行輸出與時間順序

這個程式不會等待,立刻輸出:

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

1.5.本示例需要注意的點

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

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

如果什麼任務都沒有,它會馬上返回。

1.6.io.run() 返回值

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

本例中沒有任何任務,所以返回值是:

0

2.示例 2:普通 main() 裡用 post() 投遞任務

2.1.程式目標

boost::asio::post() 可以把一個普通函式投遞到 io_context 裡,讓它以後由 io.run() 執行。

2.2.完整程式碼

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

void print_msg(const std::string& msg)
{
    std::cout << "执行任务:" << msg << std::endl;
}

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

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

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

    std::cout << "main:准备调用 io.run()" << std::endl;

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

    std::cout << "main:io.run() 返回,执行了 " << count << " 个任务" << std::endl;

    return 0;
}

執行結果:見下方“執行輸出與時間順序”;如果示例涉及定時器、執行緒、網路或外部裝置,具體時間和順序可能會隨環境略有變化。

2.3.編譯執行

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

2.4.執行輸出與時間順序

程式立刻輸出:

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

2.5.本示例需要注意的點

post() 不是立刻執行函式,而是把函式放進 io_context 的任務佇列。

真正執行發生在:

io.run();

2.6.post() 函式說明

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

引數:

  1. io:任務投遞到哪個 io_context
  2. handler:將來要執行的函式物件。

返回值:通常不用關心。

2.7.std::bind 說明

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

表示生成一個“無引數函式物件”。以後執行它時,相當於執行:

print_msg("A");

3.示例 3:work_guardio.run() 不會因為沒任務而立刻退出

3.1.程式目標

很多機器人程式裡,Asio 通訊執行緒需要長期執行。
如果暫時沒有任務,io.run() 可能直接返回,通訊執行緒就結束了。

executor_work_guard 可以告訴 io_context

先别退出,我后面可能还会投递任务。

3.2.完整程式碼

#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <chrono>
#include <functional>
#include <iostream>
#include <memory>

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

void release_guard(const boost::system::error_code& ec,
                   std::shared_ptr<WorkGuard> guard)
{
    if (ec)
    {
        std::cout << "release_guard 定时器取消:" << ec.message() << std::endl;
        return;
    }

    std::cout << "2 秒到了:释放 work_guard" << std::endl;
    guard->reset();
}

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

    std::shared_ptr<WorkGuard> guard =
        std::make_shared<WorkGuard>(boost::asio::make_work_guard(io));

    // 创建定时器,并设置到期时间。
    boost::asio::steady_timer timer(io, std::chrono::seconds(2));

    // 注册异步等待:这一行不会阻塞,回调会在定时器到期后执行。
    timer.async_wait(std::bind(release_guard,
                               std::placeholders::_1,
                               guard));

    std::cout << "main:有 work_guard,io.run() 不会空转退出" << std::endl;

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

    std::cout << "main:io.run() 返回,执行了 " << count << " 个回调" << std::endl;

    return 0;
}

執行結果:見下方“執行輸出與時間順序”;如果示例涉及定時器、執行緒、網路或外部裝置,具體時間和順序可能會隨環境略有變化。

3.3.編譯執行

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

3.4.執行輸出與時間順序

程式立刻輸出:

main:有 work_guard,io.run() 不会空转退出

約 2 秒後輸出:

2 秒到了:释放 work_guard
main:io.run() 返回,执行了 1 个回调

完整輸出類似:

main:有 work_guard,io.run() 不会空转退出
2 秒到了:释放 work_guard
main:io.run() 返回,执行了 1 个回调

3.5.本示例需要注意的點

如果只有 work_guard,但沒有 timer 釋放它,那麼 io.run() 會一直不返回。

機器人通訊執行緒裡經常會這麼做:

创建 io_context
创建 work_guard
开一个线程 run()
程序退出时 reset guard + stop io_context

3.6.make_work_guard() 說明

boost::asio::make_work_guard(io)

作用:建立一個 guard,讓 io_context 認為“還有工作沒完成”。

guard.reset() 後,如果此時沒有其他任務,io.run() 就可以返回。


4.示例 4:std::bind 的普通函式引數繫結

4.1.程式目標

專門練習 std::bind,看清楚 _1、固定引數、引數順序。

4.2.完整程式碼

#include <functional>
#include <iostream>
#include <string>

void print_robot_state(const std::string& name, double x, double y)
{
    std::cout << "机器人:" << name << ",x = " << x << ",y = " << y << std::endl;
}

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    // std::function 可以保存普通函数、lambda 或函数对象。
    std::function<void(double, double)> f =
        // bind 会把函数和部分参数提前绑定成一个可调用对象。
        std::bind(print_robot_state,
                  std::string("mycar"),
                  std::placeholders::_1,
                  std::placeholders::_2);

    std::cout << "main:准备调用绑定后的函数" << std::endl;

    f(1.2, 3.4);
    f(5.6, 7.8);

    std::cout << "main:结束" << std::endl;

    return 0;
}

執行結果:見下方“執行輸出與時間順序”;如果示例涉及定時器、執行緒、網路或外部裝置,具體時間和順序可能會隨環境略有變化。

4.3.編譯執行

g++ demo4_bind_basic.cpp -o demo4_bind_basic -std=c++17
./demo4_bind_basic

4.4.執行輸出

main:准备调用绑定后的函数
机器人:mycar,x = 1.2,y = 3.4
机器人:mycar,x = 5.6,y = 7.8
main:结束

4.5.本示例需要注意的點

這句:

std::bind(print_robot_state,
          std::string("mycar"),
          std::placeholders::_1,
          std::placeholders::_2)

表示:

第 1 个参数固定成 "mycar"
未来传入的第 1 个参数放到 x
未来传入的第 2 个参数放到 y

所以:

f(1.2, 3.4);

等價於:

print_robot_state("mycar", 1.2, 3.4);

5.示例 5:類成員函式使用 std::bind

5.1.程式目標

看清楚成員函式為什麼要寫:

&ClassName::function_name
this

5.2.完整程式碼

#include <functional>
#include <iostream>
#include <string>

class Robot
{
public:
    explicit Robot(const std::string& name)
        : name_(name)
    {
    }

    void print_pose(double x, double y)
    {
        std::cout << "机器人:" << name_ << ",x = " << x << ",y = " << y << std::endl;
    }

private:
    std::string name_;
};

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    Robot robot("mycar");

    // std::function 可以保存普通函数、lambda 或函数对象。
    std::function<void(double, double)> f =
        // bind 会把函数和部分参数提前绑定成一个可调用对象。
        std::bind(&Robot::print_pose,
                  &robot,
                  std::placeholders::_1,
                  std::placeholders::_2);

    std::cout << "main:准备调用成员函数绑定对象" << std::endl;

    f(1.0, 2.0);
    f(3.0, 4.0);

    std::cout << "main:结束" << std::endl;

    return 0;
}

執行結果:見下方“執行輸出與時間順序”;如果示例涉及定時器、執行緒、網路或外部裝置,具體時間和順序可能會隨環境略有變化。

5.3.編譯執行

g++ demo5_bind_member.cpp -o demo5_bind_member -std=c++17
./demo5_bind_member

5.4.執行輸出

main:准备调用成员函数绑定对象
机器人:mycar,x = 1,y = 2
机器人:mycar,x = 3,y = 4
main:结束

5.5.本示例需要注意的點

普通函式繫結:

std::bind(print_robot_state, ...)

成員函式繫結:

std::bind(&Robot::print_pose, &robot, ...)

原因:成員函式必須依賴某個物件才能呼叫。

5.6.&Robot::print_pose 是什麼

它是成員函式指標,表示“Robot 類裡的 print_pose 函式”。

但它還沒有指定具體物件。

5.7.&robot 是什麼

它表示具體呼叫哪個物件的成員函式。

所以完整含義是:

未来调用 robot.print_pose(x, y)。

6.示例 6:buffer 的基本使用

6.1.程式目標

Boost.Asio 讀寫資料時經常寫:

boost::asio::buffer(data)

本例先看它的基本含義。

6.2.完整程式碼

#include <boost/asio.hpp>
#include <array>
#include <iostream>
#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::cout << "msg.size() = " << msg.size() << std::endl;
    std::cout << "buffer(msg).size() = " << buf1.size() << std::endl;
    std::cout << "array size = " << data.size() << std::endl;
    std::cout << "buffer(data).size() = " << buf2.size() << std::endl;

    return 0;
}

執行結果:見下方“執行輸出與時間順序”;如果示例涉及定時器、執行緒、網路或外部裝置,具體時間和順序可能會隨環境略有變化。

6.3.編譯執行

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

6.4.執行輸出

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

6.5.本示例需要注意的點

boost::asio::buffer() 通常不會複製資料,它只是生成一個“指向已有記憶體的 buffer 檢視”。

所以非同步讀寫時特別注意:

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

錯誤寫法示意:

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

這個 msgsend() 返回後就銷燬了,但非同步寫可能還沒完成。

正確思路:

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

7.示例 7:類裡封裝 io_context 工作執行緒

7.1.程式目標

工程裡經常希望 Asio 在單獨執行緒執行。
本例寫一個最小的 IoThread 類:

  1. 構造時建立 work_guard
  2. start() 開執行緒執行 io.run()
  3. stop() 停止執行緒;
  4. post() 投遞任務驗證效果。

7.2.完整程式碼

#include <boost/asio.hpp>
#include <functional>
#include <iostream>
#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::cout << "IoThread:io.run() 开始" << std::endl;
        // 启动事件循环,前面注册的异步任务会在这里被调度执行。
        io_.run();
        std::cout << "IoThread:io.run() 返回" << std::endl;
    }

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::cout << "执行任务:" << msg << std::endl;
}

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::cout << "main:准备停止 IoThread" << std::endl;
    io_thread.stop();

    std::cout << "main:结束" << std::endl;

    return 0;
}

執行結果:見下方“執行輸出與時間順序”;如果示例涉及定時器、執行緒、網路或外部裝置,具體時間和順序可能會隨環境略有變化。

7.3.編譯執行

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

7.4.執行輸出與時間順序

程式啟動後,工作執行緒開始執行:

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

約 1 秒後主執行緒停止它:

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

完整輸出類似:

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

7.5.本示例需要注意的點

這個類就是以後封裝序列埠、TCP、UDP 通訊執行緒的雛形。

但注意:

io_.stop();

會讓 io_context 儘快停止,未完成的非同步任務可能不會正常完成。

工程裡更細緻的做法是:

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

8.本節總結

  1. 沒有任務時,io.run() 立刻返回。
  2. post() 可以把普通函式投遞給 io_context 執行。
  3. work_guard 可以防止 io.run() 因為沒任務而退出。
  4. std::bind 可以繫結普通函式,也可以繫結成員函式。
  5. boost::asio::buffer() 通常只是記憶體檢視,不負責延長資料生命週期。
  6. 非同步工程裡最重要的是物件生命週期和 buffer 生命週期。