定时器与异步 IO
本节是整套 Boost.Asio 教程最重要的一节。
串口、TCP、UDP 的异步模型都和定时器类似:先注册异步操作,然后由io_context.run()驱动回调执行。
本节所有异步回调都使用 std::bind,暂时不使用 lambda。
示例 1:普通 main() 里写同步定时器
程序目标
先写最简单的阻塞式定时器:程序启动后立刻打印一句话,然后阻塞等待 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;
}
运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。
运行输出与时间顺序
程序开始:准备等待 2 秒
等待约 2 秒后:
2 秒到了:timer.wait() 返回
完整输出类似:
程序开始:准备等待 2 秒
2 秒到了:timer.wait() 返回
本示例需要注意的点
这个程序没有体现“异步”,因为 timer.wait() 会直接阻塞当前线程。
也就是说,程序卡在这里:
timer.wait();
2 秒没有到之前,下一行不会执行。
关键函数说明
boost::asio::io_context io;
作用:创建一个 Asio 事件循环对象。
在同步 wait() 示例中,它看起来没什么存在感;但到了异步版本,所有异步事件都要靠它驱动。
boost::asio::steady_timer timer(io, std::chrono::seconds(2));
作用:创建一个基于稳定时钟的定时器,设置 2 秒后到期。
参数:
io
表示这个 timer 归哪个 io_context 管。
std::chrono::seconds(2)
表示相对当前时间 2 秒后到期。
timer.wait();
作用:阻塞等待 timer 到期。
返回值:void。
特点:简单,但是会阻塞当前线程。机器人程序里如果在主控制循环里这么写,很容易卡住整个程序。
示例 2:普通 main() 里写异步定时器
程序目标
把示例 1 改成异步写法:
- 注册一个 2 秒定时器;
async_wait()不阻塞;io.run()开始阻塞;- 2 秒后回调函数被执行;
- 没有任务后
io.run()返回。
完整代码
#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;
}
运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。
运行输出与时间顺序
程序立刻输出:
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() 返回,程序结束
本示例需要注意的点
async_wait() 不会等待 2 秒。它只是把“2 秒后执行回调”这件事注册到 io_context 里。
真正等待的是:
io.run();
所以不要把 async_wait() 理解成“异步版本的 wait”。更准确地说:
async_wait() = 注册任务
io.run() = 执行事件循环
on_timer() = 任务完成后的回调
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)
关键函数说明
timer.async_wait(handler)
作用:注册一个异步等待操作。
参数:一个回调函数对象。
返回值:void。
注意:它不会阻塞等待定时器到期,只负责注册。
io.run()
作用:启动事件循环,处理异步任务和回调函数。
返回值:执行过的 handler 数量,类型通常可以看成 std::size_t。
本例中,io.run() 执行了 1 个回调,所以返回值是 1。只是我们没有打印返回值。
示例 3:普通 main() 里写两个异步定时器
程序目标
一个 timer 很难看出异步调度的感觉,所以这次写两个 timer:
timer11 秒后到期;timer23 秒后到期;- 两个 timer 都在同一个
io_context里; io.run()会等两个任务都完成后才返回。
完整代码
#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;
}
运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。
运行输出与时间顺序
程序立刻输出:
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 个回调
本示例需要注意的点
两个 timer 不是两个线程。
本例只有一个线程,也就是主线程。timer1 和 timer2 的回调都在调用 io.run() 的这个主线程里执行。
真正的执行顺序是:
主线程进入 io.run()
等待事件
timer1 先到期 -> 调用 timer1 回调
继续等待事件
timer2 后到期 -> 调用 timer2 回调
没有任务了 -> io.run() 返回
他和std::this_thread::sleep_for()不一样,线程里的睡眠是一个阻塞式延迟,而这里的定时器只是一个事件,类似于中断。
std::bind 里的固定参数
这句:
std::bind(on_timer, std::placeholders::_1, std::string("timer1"))
等价于提前准备了一个函数对象:
未来 Asio 传入 ec 时,调用 on_timer(ec, "timer1")。
_1 是未来由 Asio 传入的错误码。
"timer1" 是现在就固定好的参数。
示例 4:普通 main() 里写重复定时器
程序目标
实现一个每 1 秒打印一次的定时器,打印 5 次后结束。
这个例子很像机器人里的周期任务,例如:
每 20ms 发送一次速度命令
每 100ms 检查一次下位机心跳
每 1s 打印一次通信状态
完整代码
#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;
}
运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。
运行输出与时间顺序
程序立刻输出:
main:注册第 1 次定时器
约 1 秒后:
第 1 次定时器回调
之后每隔约 1 秒输出一次:
第 2 次定时器回调
第 3 次定时器回调
第 4 次定时器回调
第 5 次定时器回调
已经执行 5 次,不再重新注册定时器
main:io.run() 返回,程序结束
完整输出类似:
main:注册第 1 次定时器
第 1 次定时器回调
第 2 次定时器回调
第 3 次定时器回调
第 4 次定时器回调
第 5 次定时器回调
已经执行 5 次,不再重新注册定时器
main:io.run() 返回,程序结束
本示例需要注意的点
重复定时器不是 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()会导致回调基本会立刻执行。
指针生命周期说明
本例里传了:
&timer
&count
这是为了让 repeat_timer() 能修改同一个 timer 和 count。
这个写法在本例中是安全的,因为:
timer 和 count 都是 main() 里的局部变量
main() 会卡在 io.run()
io.run() 返回前,timer 和 count 都不会析构
但是工程里更推荐把它封装成类,见下一个示例。
示例 5:类里写单个异步定时器
程序目标
把示例 2 改成类封装版本。
类封装是机器人项目里更常见的写法,例如:
SerialDriver 类
TcpClient 类
UdpReceiver 类
RobotController 类
完整代码
#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;
}
运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。
运行输出与时间顺序
程序立刻输出:
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() 返回
构造函数初始化列表说明
这段:
explicit Printer(boost::asio::io_context& io)
: timer_(io, std::chrono::seconds(2))
{
}
冒号后面叫“构造函数初始化列表”。
它的作用是:在进入构造函数大括号之前,直接构造成员变量 timer_。
为什么这里必须这么写?因为 boost::asio::steady_timer 没有一个适合你先默认构造、再赋值的简单写法。它需要在构造时就知道自己属于哪个 io_context。
成员函数绑定说明
这句:
timer_.async_wait(std::bind(&Printer::on_timer,
this,
std::placeholders::_1));
含义是:
定时器到期后,调用 this->on_timer(ec)。
为什么要写 &Printer::on_timer?
因为 on_timer 是成员函数,不是普通全局函数。成员函数必须依赖某个对象才能调用。
为什么要传 this?
因为 this 表示当前这个 Printer 对象。
易错点
不要写成:
std::bind(on_timer, std::placeholders::_1)
因为 on_timer 是成员函数,不是普通函数。
也不要漏掉 this:
std::bind(&Printer::on_timer, std::placeholders::_1)
这样不知道要调用哪个对象的 on_timer()。
示例 6:类里写两个定时器
程序目标
类里同时维护两个 timer:
timer1_1 秒后执行;timer2_3 秒后执行;- 用这个例子看清楚“两个异步任务不等于两个线程”。
完整代码
#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;
}
运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。
运行输出与时间顺序
程序立刻输出:
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() 返回
本示例需要注意的点
timer1_ 和 timer2_ 是两个异步任务,但本例仍然只有一个线程。
因为只有主线程调用了:
io.run();
所以两个回调都在主线程里执行,不会并行执行。
示例 7:两个线程同时运行同一个 io_context
程序目标
这次让两个线程都调用 io.run(),观察回调可能由不同线程执行。
这个例子用于理解官方 Timer.5 那类程序:
多个线程 run 同一个 io_context
回调函数可能分配给任意一个正在 run 的线程执行
完整代码
#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;
}
运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。
运行输出与时间顺序
程序启动后两个线程几乎立刻输出:
线程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:两个线程都结束
也不一定哪个定时器的回调由哪个线程执行,这是不确定的。 也可能两个回调都被同一个线程执行。线程调度不保证固定。
本示例需要注意的点
这个示例的重点是:
谁调用 io.run(),谁就有可能执行回调函数。
不是 timer1 固定属于线程 1,timer2 固定属于线程 2。
std::thread 里的 std::bind
这句:
std::thread t1(std::bind(run_io, &io, std::string("线程1")));
意思是创建一个线程,在新线程里执行:
run_io(&io, "线程1");
这里也没有使用 lambda。
示例 8:多线程下使用 strand 避免回调并发
程序目标
如果只有一个线程执行:
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 仍然只能:
一个执行完
↓
另一个再执行
完整代码
#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;
}
如果没有 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。
加上 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 ]
↑
同时执行
strand 不是“固定线程”
这是理解 strand 最重要的一点之一。
strand 并不是说:
所有绑定到 strand 的 handler
永远都在线程1执行
例如完全可能:
timer1 handler
→ 在线程1执行
timer2 handler
→ 在线程2执行
只要它们:
不同时执行
就是允许的。
所以:
strand
≠ 指定一个固定线程
而是:
strand
= 保证一组 handler 串行执行
换句话说:
strand 管的是“执行时间不能重叠”,不是“必须在哪个线程上执行”。
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 同时运行。
为什么这里的 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
避免了这里的并发访问。
但 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 的串行化规则。
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_的串行执行规则。
运行输出与时间顺序
程序启动以后:
线程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 不会同时执行。
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 更适合:一个对象的一系列异步事件,本来就应该一个一个处理。
本示例最重要的结论
如果一个 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 不会彼此并发执行。
本节总结
你现在应该记住:
timer.wait()是同步阻塞。timer.async_wait()是注册异步任务,不阻塞。io.run()才是事件循环的入口。- 一个线程
run(),回调就在这个线程里顺序执行。 - 多个线程
run(),回调可能被不同线程执行。 - 多线程共享数据时,用
strand避免回调并发。 - 类里绑定成员函数的标准写法是:
std::bind(&ClassName::member_function,
this,
std::placeholders::_1,
std::placeholders::_2)