Boost.Asio 基礎
本節不急着講串口、TCP、UDP,而是把
io_context、run()、post()、work_guard、buffer、std::bind、對象生命週期這些基礎講清楚。
這些東西如果沒搞懂,後面串口和網絡程序會出現“為什麼回調不執行”“為什麼程序直接退出”“為什麼段錯誤”等問題。
示例 1:io_context 沒有任務時,run() 立刻返回
程序目標
驗證一個非常重要的現象:如果 io_context 裏沒有任何未完成任務,io.run() 會立刻返回。
完整代碼
#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;
}
運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。
編譯運行
g++ demo1_empty_run.cpp -o demo1_empty_run -std=c++17 -lboost_system -pthread
./demo1_empty_run
運行輸出與時間順序
這個程序不會等待,立刻輸出:
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 <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;
}
運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。
編譯運行
g++ demo2_post.cpp -o demo2_post -std=c++17 -lboost_system -pthread
./demo2_post
運行輸出與時間順序
程序立刻輸出:
main:投递任务 A
main:投递任务 B
main:准备调用 io.run()
执行任务:A
执行任务:B
main:io.run() 返回,执行了 2 个任务
本示例需要注意的點
post() 不是立刻執行函數,而是把函數放進 io_context 的任務隊列。
真正執行發生在:
io.run();
post() 函數説明
boost::asio::post(io, handler);
參數:
io:任務投遞到哪個io_context;handler:將來要執行的函數對象。
返回值:通常不用關心。
std::bind 説明
std::bind(print_msg, std::string("A"))
表示生成一個“無參數函數對象”。以後執行它時,相當於執行:
print_msg("A");
示例 3:work_guard 讓 io.run() 不會因為沒任務而立刻退出
程序目標
很多機器人程序裏,Asio 通信線程需要長期運行。
如果暫時沒有任務,io.run() 可能直接返回,通信線程就結束了。
executor_work_guard 可以告訴 io_context:
先别退出,我后面可能还会投递任务。
完整代碼
#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;
}
運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。
編譯運行
g++ demo3_work_guard.cpp -o demo3_work_guard -std=c++17 -lboost_system -pthread
./demo3_work_guard
運行輸出與時間順序
程序立刻輸出:
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 个回调
本示例需要注意的點
如果只有 work_guard,但沒有 timer 釋放它,那麼 io.run() 會一直不返回。
機器人通信線程裏經常會這麼做:
创建 io_context
创建 work_guard
开一个线程 run()
程序退出时 reset guard + stop io_context
make_work_guard() 説明
boost::asio::make_work_guard(io)
作用:創建一個 guard,讓 io_context 認為“還有工作沒完成”。
guard.reset() 後,如果此時沒有其他任務,io.run() 就可以返回。
示例 4:std::bind 的普通函數參數綁定
程序目標
專門練習 std::bind,看清楚 _1、固定參數、參數順序。
完整代碼
#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;
}
運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。
編譯運行
g++ demo4_bind_basic.cpp -o demo4_bind_basic -std=c++17
./demo4_bind_basic
運行輸出
main:准备调用绑定后的函数
机器人:mycar,x = 1.2,y = 3.4
机器人:mycar,x = 5.6,y = 7.8
main:结束
本示例需要注意的點
這句:
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:類成員函數使用 std::bind
程序目標
看清楚成員函數為什麼要寫:
&ClassName::function_name
this
完整代碼
#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;
}
運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。
編譯運行
g++ demo5_bind_member.cpp -o demo5_bind_member -std=c++17
./demo5_bind_member
運行輸出
main:准备调用成员函数绑定对象
机器人:mycar,x = 1,y = 2
机器人:mycar,x = 3,y = 4
main:结束
本示例需要注意的點
普通函數綁定:
std::bind(print_robot_state, ...)
成員函數綁定:
std::bind(&Robot::print_pose, &robot, ...)
原因:成員函數必須依賴某個對象才能調用。
&Robot::print_pose 是什麼
它是成員函數指針,表示“Robot 類裏的 print_pose 函數”。
但它還沒有指定具體對象。
&robot 是什麼
它表示具體調用哪個對象的成員函數。
所以完整含義是:
未来调用 robot.print_pose(x, y)。
示例 6:buffer 的基本使用
程序目標
Boost.Asio 讀寫數據時經常寫:
boost::asio::buffer(data)
本例先看它的基本含義。
完整代碼
#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;
}
運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。
編譯運行
g++ demo6_buffer.cpp -o demo6_buffer -std=c++17 -lboost_system -pthread
./demo6_buffer
運行輸出
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 管理
或者使用写队列保存待发送数据
示例 7:類裏封裝 io_context 工作線程
程序目標
工程裏經常希望 Asio 在單獨線程運行。
本例寫一個最小的 IoThread 類:
- 構造時創建
work_guard; start()開線程執行io.run();stop()停止線程;- 用
post()投遞任務驗證效果。
完整代碼
#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;
}
運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。
編譯運行
g++ demo7_iothread.cpp -o demo7_iothread -std=c++17 -lboost_system -pthread
./demo7_iothread
運行輸出與時間順序
程序啓動後,工作線程開始運行:
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() 返回
本節總結
- 沒有任務時,
io.run()立刻返回。 post()可以把普通函數投遞給io_context執行。work_guard可以防止io.run()因為沒任務而退出。std::bind可以綁定普通函數,也可以綁定成員函數。boost::asio::buffer()通常只是內存視圖,不負責延長數據生命週期。- 異步工程裏最重要的是對象生命週期和 buffer 生命週期。