Wiki
定時器與異步 IO
本文由簡體中文內容確定性轉換,並受版本化術語表保護。
本節是整套 Boost.Asio 教程最重要的一節。
串口、TCP、UDP 的異步模型都和定時器類似:先註冊異步操作,然後由io_context.run()驅動回調執行。
本節所有異步回調都使用 std::bind,暫時不使用 lambda。
1.示例 1:普通 main() 裏寫同步定時器
1.1.程序目標
先寫最簡單的阻塞式定時器:程序啓動後立刻打印一句話,然後阻塞等待 2 秒,再打印結束。
1.2.完整代碼
#include <boost/asio.hpp>
#include <chrono>
#include <print>
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
// io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
boost::asio::io_context io;
// 创建定时器,并设置到期时间。
boost::asio::steady_timer timer(io, std::chrono::seconds(2));
std::println("程序开始:准备等待 2 秒");
// 同步等待会阻塞当前线程,时间没到之前不会继续往下执行。
timer.wait();
std::println("2 秒到了:timer.wait() 返回");
return 0;
}
運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。
1.3.運行輸出與時間順序
程序开始:准备等待 2 秒
等待約 2 秒後:
2 秒到了:timer.wait() 返回
完整輸出類似:
程序开始:准备等待 2 秒
2 秒到了:timer.wait() 返回
1.4.本示例需要注意的點
這個程序沒有體現“異步”,因為 timer.wait() 會直接阻塞當前線程。
也就是説,程序卡在這裏:
timer.wait();
2 秒沒有到之前,下一行不會執行。
1.5.關鍵函數説明
1.5.1.boost::asio::io_context io;
作用:創建一個 Asio 事件循環對象。
在同步 wait() 示例中,它看起來沒什麼存在感;但到了異步版本,所有異步事件都要靠它驅動。
1.5.2.boost::asio::steady_timer timer(io, std::chrono::seconds(2));
作用:創建一個基於穩定時鐘的定時器,設置 2 秒後到期。
參數:
io
表示這個 timer 歸哪個 io_context 管。
std::chrono::seconds(2)
表示相對當前時間 2 秒後到期。
1.5.3.timer.wait();
作用:阻塞等待 timer 到期。
返回值:void。
特點:簡單,但是會阻塞當前線程。機械人程序裏如果在主控制循環裏這麼寫,很容易卡住整個程序。
2.示例 2:普通 main() 裏寫異步定時器
2.1.程序目標
把示例 1 改成異步寫法:
- 註冊一個 2 秒定時器;
async_wait()不阻塞;io.run()開始阻塞;- 2 秒後回調函數被執行;
- 沒有任務後
io.run()返回。
2.2.完整代碼
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <chrono>
#include <functional>
#include <print>
void on_timer(const boost::system::error_code& ec)
{
if (ec)
{
std::println("定时器被取消,错误信息:{}", ec.message());
return;
}
std::println("回调函数 on_timer:2 秒到了");
}
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
// io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
boost::asio::io_context io;
// 创建定时器,并设置到期时间。
boost::asio::steady_timer timer(io, std::chrono::seconds(2));
std::println("main:注册 async_wait");
// 注册异步等待:这一行不会阻塞,回调会在定时器到期后执行。
timer.async_wait(std::bind(on_timer, std::placeholders::_1));
std::println("main:async_wait 已经返回,但回调还没执行");
std::println("main:准备调用 io.run()");
io.run();
std::println("main:io.run() 返回,程序结束");
return 0;
}
運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。
2.3.運行輸出與時間順序
程序立刻輸出:
main:注册 async_wait
main:async_wait 已经返回,但回调还没执行
main:准备调用 io.run()
然後程序會卡在:
io.run();
等待約 2 秒後輸出:
回调函数 on_timer:2 秒到了
main:io.run() 返回,程序结束
完整輸出類似:
main:注册 async_wait
main:async_wait 已经返回,但回调还没执行
main:准备调用 io.run()
回调函数 on_timer:2 秒到了
main:io.run() 返回,程序结束
2.4.本示例需要注意的點
async_wait() 不會等待 2 秒。它只是把“2 秒後執行回調”這件事註冊到 io_context 裏。
真正等待的是:
io.run();
所以不要把 async_wait() 理解成“異步版本的 wait”。更準確地説:
async_wait() = 注册任务
io.run() = 执行事件循环
on_timer() = 任务完成后的回调
2.5.std::bind 説明
這句代碼:
timer.async_wait(std::bind(on_timer, std::placeholders::_1));
意思是:
当 timer 到期后,Boost.Asio 会传入一个 error_code。
这个 error_code 会填到 std::placeholders::_1 的位置。
最后调用 on_timer(ec)。
on_timer 的函數簽名必須能接收這個參數:
void on_timer(const boost::system::error_code& ec)
2.6.關鍵函數説明
2.6.1.timer.async_wait(handler)
作用:註冊一個異步等待操作。
參數:一個回調函數對象。
返回值:void。
注意:它不會阻塞等待定時器到期,只負責註冊。
2.6.2.io.run()
作用:啓動事件循環,處理異步任務和回調函數。
返回值:執行過的 handler 數量,類型通常可以看成 std::size_t。
本例中,io.run() 執行了 1 個回調,所以返回值是 1。只是我們沒有打印返回值。
3.示例 3:普通 main() 裏寫兩個異步定時器
3.1.程序目標
一個 timer 很難看出異步調度的感覺,所以這次寫兩個 timer:
timer11 秒後到期;timer23 秒後到期;- 兩個 timer 都在同一個
io_context裏; io.run()會等兩個任務都完成後才返回。
3.2.完整代碼
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <chrono>
#include <functional>
#include <print>
#include <string>
void on_timer(const boost::system::error_code& ec, const std::string& name)
{
if (ec)
{
std::println("{} 被取消:{}", name, ec.message());
return;
}
std::println("{} 到期,执行回调", name);
}
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
// io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
boost::asio::io_context io;
// 创建定时器,并设置到期时间。
boost::asio::steady_timer timer1(io, std::chrono::seconds(1));
boost::asio::steady_timer timer2(io, std::chrono::seconds(3));
std::println("main:注册 timer1,1 秒后到期");
// 注册异步等待:这一行不会阻塞,回调会在定时器到期后执行。
timer1.async_wait(std::bind(on_timer,
std::placeholders::_1,
std::string("timer1")));
std::println("main:注册 timer2,3 秒后到期");
timer2.async_wait(std::bind(on_timer,
std::placeholders::_1,
std::string("timer2")));
std::println("main:调用 io.run()");
std::size_t count = io.run();
std::println("main:io.run() 返回,一共执行了 {} 个回调", count);
return 0;
}
運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。
3.3.運行輸出與時間順序
程序立刻輸出:
main:注册 timer1,1 秒后到期
main:注册 timer2,3 秒后到期
main:调用 io.run()
約 1 秒後輸出:
timer1 到期,执行回调
再過約 2 秒,也就是程序啓動後約 3 秒,輸出:
timer2 到期,执行回调
main:io.run() 返回,一共执行了 2 个回调
完整輸出類似:
main:注册 timer1,1 秒后到期
main:注册 timer2,3 秒后到期
main:调用 io.run()
timer1 到期,执行回调
timer2 到期,执行回调
main:io.run() 返回,一共执行了 2 个回调
3.4.本示例需要注意的點
兩個 timer 不是兩個線程。
本例只有一個線程,也就是主線程。timer1 和 timer2 的回調都在調用 io.run() 的這個主線程裏執行。
真正的執行順序是:
主线程进入 io.run()
等待事件
timer1 先到期 -> 调用 timer1 回调
继续等待事件
timer2 后到期 -> 调用 timer2 回调
没有任务了 -> io.run() 返回
他和std::this_thread::sleep_for()不一樣,線程裏的睡眠是一個阻塞式延遲,而這裏的定時器只是一個事件,類似於中斷。
3.5.std::bind 裏的固定參數
這句:
std::bind(on_timer, std::placeholders::_1, std::string("timer1"))
等價於提前準備了一個函數對象:
未来 Asio 传入 ec 时,调用 on_timer(ec, "timer1")。
_1 是未來由 Asio 傳入的錯誤碼。
"timer1" 是現在就固定好的參數。
4.示例 4:普通 main() 裏寫重複定時器
4.1.程序目標
實現一個每 1 秒打印一次的定時器,打印 5 次後結束。
這個例子很像機械人裏的週期任務,例如:
每 20ms 发送一次速度命令
每 100ms 检查一次下位机心跳
每 1s 打印一次通信状态
4.2.完整代碼
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <chrono>
#include <functional>
#include <print>
void repeat_timer(const boost::system::error_code& ec,
boost::asio::steady_timer* timer,
int* count)
{
if (ec)
{
std::println("repeat_timer 被取消:{}", ec.message());
return;
}
++(*count);
std::println("第 {} 次定时器回调", *count);
if (*count < 5)
{
// 重新设置下一次到期时间,并再次注册异步等待,使定时器继续周期执行。
timer->expires_after(std::chrono::seconds(1));
timer->async_wait(std::bind(repeat_timer,
std::placeholders::_1,
timer,
count));
}
else
{
std::println("已经执行 5 次,不再重新注册定时器");
}
}
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
// io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
boost::asio::io_context io;
// 创建定时器,并设置到期时间。
boost::asio::steady_timer timer(io, std::chrono::seconds(1));
int count = 0;
std::println("main:注册第 1 次定时器");
// 注册异步等待:这一行不会阻塞,回调会在定时器到期后执行。
timer.async_wait(std::bind(repeat_timer,
std::placeholders::_1,
&timer,
&count));
io.run();
std::println("main:io.run() 返回,程序结束");
return 0;
}
運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。
4.3.運行輸出與時間順序
程序立刻輸出:
main:注册第 1 次定时器
約 1 秒後:
第 1 次定时器回调
之後每隔約 1 秒輸出一次:
第 2 次定时器回调
第 3 次定时器回调
第 4 次定时器回调
第 5 次定时器回调
已经执行 5 次,不再重新注册定时器
main:io.run() 返回,程序结束
完整輸出類似:
main:注册第 1 次定时器
第 1 次定时器回调
第 2 次定时器回调
第 3 次定时器回调
第 4 次定时器回调
第 5 次定时器回调
已经执行 5 次,不再重新注册定时器
main:io.run() 返回,程序结束
4.4.本示例需要注意的點
重複定時器不是 while 循環裏 sleep。
異步寫法是:
第一次 async_wait
第 1 次回调里重新设置 expires_after
第 1 次回调里再次 async_wait
第 2 次回调里再次注册
……
也就是“每次回調結束前,決定要不要註冊下一次”。
// 重新设置下一次到期时间,并再次注册异步等待,使定时器继续周期执行。
timer->expires_after(std::chrono::seconds(1));
timer->async_wait(std::bind(repeat_timer,
std::placeholders::_1,
timer,
count));
上面這兩行很重要:
timer->expires_after(std::chrono::seconds(1));是把定時器的到期時間重新設置為“從現在開始 1 秒後”。它本身不會註冊回調,也不會阻塞線程,只是修改定時器的到期時間。
timer->async_wait(std::bind(repeat_timer,std::placeholders::_1,timer,count));決定了到期以後執行誰。
因此這兩行可以理解成:“給下一輪定時器續命”。
boost::asio::steady_timer timer(io, std::chrono::seconds(1));只是把第一次到期時間設成“現在之後 1 秒”。第一次到期以後,如果你不再調用上面這兩行,那麼定時器的到期時間還是原來的那個時間點,已經過去了。
如果你不用expires_after()設定新的時間,那麼直接調用async_wait()會導致回調基本會立刻執行。
4.5.指針生命週期説明
本例裏傳了:
&timer
&count
這是為了讓 repeat_timer() 能修改同一個 timer 和 count。
這個寫法在本例中是安全的,因為:
timer 和 count 都是 main() 里的局部变量
main() 会卡在 io.run()
io.run() 返回前,timer 和 count 都不会析构
但是工程裏更推薦把它封裝成類,見下一個示例。
5.示例 5:類裏寫單個異步定時器
5.1.程序目標
把示例 2 改成類封裝版本。
類封裝是機械人項目裏更常見的寫法,例如:
SerialDriver 类
TcpClient 类
UdpReceiver 类
RobotController 类
5.2.完整代碼
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <chrono>
#include <functional>
#include <print>
class Printer
{
public:
explicit Printer(boost::asio::io_context& io)
: timer_(io, std::chrono::seconds(2))
{
std::println("Printer 构造:注册 2 秒定时器");
// 注册异步等待:这一行不会阻塞,回调会在定时器到期后执行。
timer_.async_wait(std::bind(&Printer::on_timer,
this,
std::placeholders::_1));
}
private:
void on_timer(const boost::system::error_code& ec)
{
if (ec)
{
std::println("Printer::on_timer 被取消:{}", ec.message());
return;
}
std::println("Printer::on_timer:2 秒到了");
}
private:
// 创建定时器,并设置到期时间。
boost::asio::steady_timer timer_;
};
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
// io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
boost::asio::io_context io;
Printer printer(io);
std::println("main:准备调用 io.run()");
io.run();
std::println("main:io.run() 返回");
return 0;
}
運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。
5.3.運行輸出與時間順序
程序立刻輸出:
Printer 构造:注册 2 秒定时器
main:准备调用 io.run()
約 2 秒後輸出:
Printer::on_timer:2 秒到了
main:io.run() 返回
完整輸出類似:
Printer 构造:注册 2 秒定时器
main:准备调用 io.run()
Printer::on_timer:2 秒到了
main:io.run() 返回
5.4.構造函數初始化列表説明
這段:
explicit Printer(boost::asio::io_context& io)
: timer_(io, std::chrono::seconds(2))
{
}
冒號後面叫“構造函數初始化列表”。
它的作用是:在進入構造函數大括號之前,直接構造成員變量 timer_。
為什麼這裏必須這麼寫?因為 boost::asio::steady_timer 沒有一個適合你先默認構造、再賦值的簡單寫法。它需要在構造時就知道自己屬於哪個 io_context。
5.5.成員函數綁定説明
這句:
timer_.async_wait(std::bind(&Printer::on_timer,
this,
std::placeholders::_1));
含義是:
定时器到期后,调用 this->on_timer(ec)。
為什麼要寫 &Printer::on_timer?
因為 on_timer 是成員函數,不是普通全局函數。成員函數必須依賴某個對象才能調用。
為什麼要傳 this?
因為 this 表示當前這個 Printer 對象。
5.6.易錯點
不要寫成:
std::bind(on_timer, std::placeholders::_1)
因為 on_timer 是成員函數,不是普通函數。
也不要漏掉 this:
std::bind(&Printer::on_timer, std::placeholders::_1)
這樣不知道要調用哪個對象的 on_timer()。
6.示例 6:類裏寫兩個定時器
6.1.程序目標
類裏同時維護兩個 timer:
timer1_1 秒後執行;timer2_3 秒後執行;- 用這個例子看清楚“兩個異步任務不等於兩個線程”。
6.2.完整代碼
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <chrono>
#include <functional>
#include <print>
class TwoTimers
{
public:
explicit TwoTimers(boost::asio::io_context& io)
: timer1_(io, std::chrono::seconds(1)),
timer2_(io, std::chrono::seconds(3))
{
std::println("TwoTimers 构造:注册 timer1 和 timer2");
// 注册异步等待:这一行不会阻塞,回调会在定时器到期后执行。
timer1_.async_wait(std::bind(&TwoTimers::on_timer1,
this,
std::placeholders::_1));
timer2_.async_wait(std::bind(&TwoTimers::on_timer2,
this,
std::placeholders::_1));
}
private:
void on_timer1(const boost::system::error_code& ec)
{
if (ec)
{
std::println("timer1 取消:{}", ec.message());
return;
}
std::println("timer1:1 秒到了");
}
void on_timer2(const boost::system::error_code& ec)
{
if (ec)
{
std::println("timer2 取消:{}", ec.message());
return;
}
std::println("timer2:3 秒到了");
}
private:
// 创建定时器,并设置到期时间。
boost::asio::steady_timer timer1_;
boost::asio::steady_timer timer2_;
};
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
// io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
boost::asio::io_context io;
TwoTimers app(io);
std::println("main:调用 io.run()");
io.run();
std::println("main:两个定时器都执行完,io.run() 返回");
return 0;
}
運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。
6.3.運行輸出與時間順序
程序立刻輸出:
TwoTimers 构造:注册 timer1 和 timer2
main:调用 io.run()
約 1 秒後:
timer1:1 秒到了
約 3 秒後:
timer2:3 秒到了
main:两个定时器都执行完,io.run() 返回
完整輸出類似:
TwoTimers 构造:注册 timer1 和 timer2
main:调用 io.run()
timer1:1 秒到了
timer2:3 秒到了
main:两个定时器都执行完,io.run() 返回
6.4.本示例需要注意的點
timer1_ 和 timer2_ 是兩個異步任務,但本例仍然只有一個線程。
因為只有主線程調用了:
io.run();
所以兩個回調都在主線程裏執行,不會並行執行。
7.示例 7:兩個線程同時運行同一個 io_context
7.1.程序目標
這次讓兩個線程都調用 io.run(),觀察回調可能由不同線程執行。
這個例子用於理解官方 Timer.5 那類程序:
多个线程 run 同一个 io_context
回调函数可能分配给任意一个正在 run 的线程执行
7.2.完整代碼
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <chrono>
#include <functional>
#include <print>
#include <string>
#include <thread>
void print_timer(const boost::system::error_code& ec, const std::string& name)
{
if (ec)
{
std::println("{} 取消:{}", name, ec.message());
return;
}
std::println("{} 回调执行,线程 id = {}", name, std::this_thread::get_id());
}
void run_io(boost::asio::io_context* io, const std::string& thread_name)
{
std::println("{} 开始调用 io.run(),线程 id = {}", thread_name, std::this_thread::get_id());
io->run();
std::println("{} 的 io.run() 返回", thread_name);
}
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
// io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
boost::asio::io_context io;
// 创建定时器,并设置到期时间。
boost::asio::steady_timer timer1(io, std::chrono::seconds(1));
boost::asio::steady_timer timer2(io, std::chrono::seconds(1));
// 注册异步等待:这一行不会阻塞,回调会在定时器到期后执行。
timer1.async_wait(std::bind(print_timer,
std::placeholders::_1,
std::string("timer1")));
timer2.async_wait(std::bind(print_timer,
std::placeholders::_1,
std::string("timer2")));
std::thread t1(std::bind(run_io, &io, std::string("线程1")));
std::thread t2(std::bind(run_io, &io, std::string("线程2")));
t1.join();
t2.join();
std::println("main:两个线程都结束");
return 0;
}
運行結果:見下方“運行輸出與時間順序”;如果示例涉及定時器、線程、網絡或外部設備,具體時間和順序可能會隨環境略有變化。
7.3.運行輸出與時間順序
程序啓動後兩個線程幾乎立刻輸出:
线程1 开始调用 io.run(),线程 id = 140000000000001
线程2 开始调用 io.run(),线程 id = 140000000000002
約 1 秒後,兩個 timer 都到期,輸出可能類似:
timer1 回调执行,线程 id = 140000000000001
timer2 回调执行,线程 id = 140000000000002
线程1 的 io.run() 返回
线程2 的 io.run() 返回
main:两个线程都结束
也不一定哪個定時器的回調由哪個線程執行,這是不確定的。 也可能兩個回調都被同一個線程執行。線程調度不保證固定。
7.4.本示例需要注意的點
這個示例的重點是:
谁调用 io.run(),谁就有可能执行回调函数。
不是 timer1 固定屬於線程 1,timer2 固定屬於線程 2。
7.5.std::thread 裏的 std::bind
這句:
std::thread t1(std::bind(run_io, &io, std::string("线程1")));
意思是創建一個線程,在新線程裏執行:
run_io(&io, "线程1");
這裏也沒有使用 lambda。
8.示例 8:多線程下使用 strand 避免回調併發
8.1.程序目標
如果只有一個線程執行:
io.run();
那麼 io_context 中的 handler 自然只能一個接一個執行。
但是如果有多個線程同時執行同一個:
io.run();
例如:
线程1 → io.run()
线程2 → io.run()
那麼 io_context 中不同的 handler 就可能被不同線程同時取走並執行。
例如兩個定時器同時到期:
线程1 → timer1 的 handler
线程2 → timer2 的 handler
如果這兩個 handler 都修改同一個普通成員變量:
int count_;
就可能出現:
线程1:++count_
线程2:++count_
同時執行,從而產生數據競爭。
Boost.Asio 提供的:
strand
就是用來解決這種 handler 併發執行 問題的。
可以先記住最核心的一句話:
綁定到同一個
strand的 handler,不會彼此併發執行。
也就是説,即使:
多个线程同时运行 io_context
同一個 strand 中的 handler 仍然只能:
一个执行完
↓
另一个再执行
8.2.完整代碼
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <chrono>
#include <functional>
#include <print>
#include <thread>
class StrandCounter
{
public:
explicit StrandCounter(boost::asio::io_context& io)
: strand_(boost::asio::make_strand(io)),
timer1_(io, std::chrono::seconds(1)),
timer2_(io, std::chrono::seconds(1)),
count_(0)
{
// timer1 到期后执行的 handler,
// 通过 bind_executor() 绑定到 strand_。
timer1_.async_wait(
boost::asio::bind_executor(
strand_,
std::bind(
&StrandCounter::on_timer,
this,
std::placeholders::_1,
1)));
// timer2 的 handler 也绑定到同一个 strand_。
timer2_.async_wait(
boost::asio::bind_executor(
strand_,
std::bind(
&StrandCounter::on_timer,
this,
std::placeholders::_1,
2)));
}
private:
void on_timer(
const boost::system::error_code& ec,
int id)
{
if (ec)
{
std::println(
"timer{} 取消:{}",
id,
ec.message());
return;
}
++count_;
std::println(
"timer{} 回调执行,count = {},线程 id = {}",
id,
count_,
std::this_thread::get_id());
}
private:
boost::asio::strand<
boost::asio::io_context::executor_type>
strand_;
boost::asio::steady_timer timer1_;
boost::asio::steady_timer timer2_;
int count_;
};
void run_io(
boost::asio::io_context* io,
const std::string& name)
{
std::println(
"{} 开始 run,线程 id = {}",
name,
std::this_thread::get_id());
io->run();
std::println("{} run 返回", name);
}
int main()
{
boost::asio::io_context io;
StrandCounter counter(io);
std::thread t1(
std::bind(
run_io,
&io,
std::string("线程1")));
std::thread t2(
std::bind(
run_io,
&io,
std::string("线程2")));
t1.join();
t2.join();
std::println("main:结束");
return 0;
}
8.3.如果沒有 strand 會發生什麼
假設我們不使用:
boost::asio::bind_executor(strand_, handler);
而是直接:
timer1_.async_wait(
std::bind(
&StrandCounter::on_timer,
this,
std::placeholders::_1,
1));
timer2_.async_wait(
std::bind(
&StrandCounter::on_timer,
this,
std::placeholders::_1,
2));
同時有兩個線程:
线程1 → io.run()
线程2 → io.run()
那麼當兩個 timer 都到期以後,完全可能出現:
线程1
↓
取到 timer1 handler
↓
进入 on_timer()
线程2
↓
取到 timer2 handler
↓
同时进入 on_timer()
也就是:
时间 ─────────────────────→
线程1: [ timer1 handler ]
线程2: [ timer2 handler ]
↑
发生重叠
兩個 handler 裏面都有:
++count_;
而:
int count_;
只是普通 int。
所以兩個線程同時修改它就會產生data race。
8.4.加上 strand 後發生什麼
我們先創建:
strand_(boost::asio::make_strand(io))
可以先把它理解成:
在這個
io_context上額外建立一條“串行執行通道”。
然後把兩個 handler 都綁定到:
strand_
例如:
boost::asio::bind_executor(
strand_,
handler);
此時:
timer1 handler ──┐
├── 同一个 strand_
timer2 handler ──┘
於是 strand 會保證:
timer1 handler
和
timer2 handler
不會同時執行。
可能是:
线程1:
timer1 handler 开始
↓
++count_
↓
timer1 handler 结束
线程2:
timer2 handler 开始
↓
++count_
↓
timer2 handler 结束
也可能反過來:
线程2:
timer2 handler
然后
线程1:
timer1 handler
但不會出現兩個 handler 的執行時間發生重疊。
可以畫成:
时间 ─────────────────────────→
线程1: [ timer1 handler ]
线程2: [ timer2 handler ]
或者:
时间 ─────────────────────────→
线程2: [ timer2 handler ]
线程1: [ timer1 handler ]
但不會:
时间 ─────────────────────────→
线程1: [ timer1 handler ]
线程2: [ timer2 handler ]
↑
同时执行
8.5.strand 不是“固定線程”
這是理解 strand 最重要的一點之一。
strand 並不是説:
所有绑定到 strand 的 handler
永远都在线程1执行
例如完全可能:
timer1 handler
→ 在线程1执行
timer2 handler
→ 在线程2执行
只要它們:
不同时执行
就是允許的。
所以:
strand
≠ 指定一个固定线程
而是:
strand
= 保证一组 handler 串行执行
換句話説:
strand 管的是“執行時間不能重疊”,不是“必須在哪個線程上執行”。
8.6.strand 也不是新的線程
strand 自己不會:
创建一个线程
它仍然依賴:
io.run();
的那些線程來真正執行 handler。
本例中真正執行 handler 的仍然只有:
线程1
线程2
strand 做的只是:
控制同一组 handler 的执行关系
可以理解成:
io_context
↓
┌──────┴──────┐
线程1 线程2
\ /
\ /
strand
↓
handler A
handler B
handler C
handler 仍然由執行 io.run() 的線程來運行,只是 strand 不允許屬於自己的兩個 handler 同時運行。
8.7.為什麼這裏的 count_ 可以不用 mutex
本例中:
++count_;
只會在:
on_timer()
中執行。
而兩個 on_timer() 對應的 handler:
timer1 handler
timer2 handler
都綁定到了同一個:
strand_
所以它們不會併發執行。
於是:
++count_;
也不會被兩個線程同時執行。
因此這裏不需要再寫:
std::mutex mutex_;
std::scoped_lock lock(mutex_);
++count_;
strand 已經通過:
串行化 handler
避免了這裏的併發訪問。
8.8.但 strand 不會自動保護整個對象
這一點也非常重要。
不能因為類裏面有:
strand_
就認為:
StrandCounter 整个对象自动线程安全
strand 只能保證:
經過這個 strand 調度的 handler 彼此不併發。
例如本例:
timer1 handler
→ strand_
timer2 handler
→ strand_
所以它們訪問:
count_
是安全的。
但如果另外有一個線程直接修改:
count_;
或者另一個沒有綁定 strand_ 的 handler 修改:
count_;
那麼 strand 管不到它。
例如:
timer1 handler
→ strand_
→ 修改 count_
另一个普通线程
→ 直接修改 count_
這兩個還是可能併發。
所以使用 strand 保護某份狀態時,需要保證:
所有可能併發訪問這份狀態的相關操作,都遵守同一個 strand 的串行化規則。
8.9.bind_executor() 是幹什麼的
這段:
boost::asio::bind_executor(
strand_,
handler);
可以理解成:
把這個 handler 的執行器綁定成
strand_。
例如:
timer1_.async_wait(
boost::asio::bind_executor(
strand_,
std::bind(
&StrandCounter::on_timer,
this,
std::placeholders::_1,
1)));
原本可以理解成:
timer1 到期
↓
handler 交给 io_context
↓
某个正在 run() 的线程执行
加入 strand 後變成:
timer1 到期
↓
handler 要经过 strand_
↓
strand_ 保证它不会和
同一个 strand 中的其他 handler 并发
↓
再由某个正在 run() 的线程执行
所以:
bind_executor(strand_, handler)
最直觀的理解就是:
讓這個 handler 遵守
strand_的串行執行規則。
8.10.運行輸出與時間順序
程序啓動以後:
线程1 开始 run,线程 id = 140000000000001
线程2 开始 run,线程 id = 140000000000002
兩個線程此時都在:
io->run();
中等待和執行 Asio 的事件。
約 1 秒後:
timer1
timer2
都到期。
可能輸出:
timer1 回调执行,count = 1,线程 id = 140000000000001
timer2 回调执行,count = 2,线程 id = 140000000000002
线程1 run 返回
线程2 run 返回
main:结束
也可能:
timer2 回调执行,count = 1,线程 id = 140000000000002
timer1 回调执行,count = 2,线程 id = 140000000000001
這些都正常。
因為 strand 不保證:
timer1 一定先执行
也不保證:
timer1 一定在线程1
timer2 一定在线程2
strand 真正保證的是:
兩個綁定到同一個 strand 的 handler 不會同時執行。
8.11.strand 和 mutex 的思路有什麼不同
如果不用 strand,也可以自己:
std::mutex mutex_;
然後:
void on_timer(...)
{
std::scoped_lock lock(mutex_);
++count_;
}
也可以避免數據競爭。
兩種思路分別是:
mutex
→ handler 可以同时开始执行
→ 但访问共享状态时竞争锁
而:
strand
→ 从 handler 调度层面就不让它们并发执行
所以在 Boost.Asio 中,如果一組異步 handler:
本来就应该按串行方式处理同一个对象状态
strand 往往是一種很自然的設計。
例如:
串口读回调
串口写完成回调
定时器回调
连接状态回调
如果它們都操作同一個連接對象的內部狀態,就可以讓這些 handler 統一經過同一個 strand。
總結:
所以 mutex 很適合:一個大函數里面,只有很小一塊共享數據需要保護。
而 strand 更適合:一個對象的一系列異步事件,本來就應該一個一個處理。
8.12.本示例最重要的結論
如果一個 io_context 只有一個線程調用:
io.run();
那麼 handler 本身不會併發執行。
但如果:
多个线程
↓
同时 io.run()
那麼不同 handler 就可能:
在不同线程上同时执行
這時:
strand
可以把某一組 handler 串行化。
最終可以記成:
多个线程 run(io_context)
↓
没有 strand
不同 handler
可能同时执行
↓
访问共享状态时
可能产生 data race
使用同一個 strand:
多个线程 run(io_context)
↓
handler 可以由不同线程执行
↓
但是同一个 strand 中的 handler
不会并发执行
↓
相关状态访问被串行化
所以一句話記住:
strand是 Boost.Asio 提供的 handler 串行化機制:它不固定線程,也不創建線程,只保證綁定到同一個 strand 的 handler 不會彼此併發執行。
9.本節總結
你現在應該記住:
timer.wait()是同步阻塞。timer.async_wait()是註冊異步任務,不阻塞。io.run()才是事件循環的入口。- 一個線程
run(),回調就在這個線程裏順序執行。 - 多個線程
run(),回調可能被不同線程執行。 - 多線程共享數據時,用
strand避免回調併發。 - 類裏綁定成員函數的標準寫法是:
std::bind(&ClassName::member_function,
this,
std::placeholders::_1,
std::placeholders::_2)