asio::steady_timer是最稳妥的定时器选择,基于单调时钟避免系统时间跳变导致的任务错乱,应优先于system_timer和已弃用的deadline_timer使用。

asio::steady_timer 是最稳妥的定时器选择
Boost.Asio 提供了两种主要定时器:asio::steady_timer 和 asio::system_timer。除非你明确需要系统时钟(比如和日志时间戳对齐),否则一律用 steady_timer——它基于单调时钟,不受系统时间跳变影响,避免定时任务突然提前或延后执行。
常见错误是直接用 system_timer 做周期性轮询,结果在 NTP 调时或手动改系统时间后,任务乱序甚至重复触发。
实操建议:
-
steady_timer构造时传入io_context和初始超时(如std::chrono::seconds(5)) - 回调必须通过
async_wait()注册,不能用wait()阻塞,否则会卡死整个io_context - 每次触发后,如需重复执行,得在回调里重新调用
async_wait()(别忘了重设到期时间)
如何安全地在回调中重启周期性定时器
很多人写成“触发 → 执行逻辑 → 立即 async_wait()”,看似没问题,但若执行逻辑耗时较长,下一次触发会被挤压,导致实际间隔越来越短,甚至堆积。
正确做法是把“重设定时器”和“业务逻辑”解耦,用固定间隔推进,而不是链式等待:
void start_periodic_timer() {
timer_.expires_after(std::chrono::seconds(1));
timer_.async_wait([this](const boost::system::error_code& ec) {
if (ec == boost::asio::error::operation_aborted) return;
do_work(); // 你的任务逻辑
// 关键:不管 do_work 耗多久,都从“上一次设定的到期点”开始算下一个周期
timer_.expires_at(timer_.expiry() + std::chrono::seconds(1));
start_periodic_timer(); // 递归调度
});
}
注意:expires_at() 比 expires_after() 更可控;递归调用比在回调末尾直接 async_wait() 更易维护边界条件。
定时器对象生命周期管理容易出错
最常见的崩溃原因是:定时器回调还在队列里,而持有它的对象(比如 class TaskManager)已经被析构了。Asio 不会自动取消未完成的异步操作。
必须显式保证定时器对象存活到回调结束,或主动取消:
- 把
asio::steady_timer成员变量放在类里,且该类生命周期长于定时器使用期 - 在类析构函数中调用
timer_.cancel(),并确保所有回调里检查ec == boost::asio::error::operation_aborted - 如果用
std::shared_ptr管理宿主对象,回调捕获shared_from_this(),防止提前释放
漏掉 cancel() 或忽略 operation_aborted 错误码,十有八九会 crash 在回调里访问已销毁成员。
不推荐用 deadline_timer(已弃用)
旧代码里常看到 asio::deadline_timer,它底层用 boost::posix_time,不仅精度差、跨平台行为不一致,而且自 Boost 1.70 起已被标记为 deprecated。新版 Asio 文档明确建议迁移到 steady_timer 或 system_timer。
迁移成本很低:
- 头文件从
改为 - 构造参数从
posix_time::seconds(5)改为std::chrono::seconds(5) - 方法名基本一致(
expires_at/expires_from_now→expires_at/expires_after)
继续用 deadline_timer 的唯一理由可能是维护十年以上的老项目,但新代码请直接绕开。
真正麻烦的不是语法转换,而是理解「定时器不是线程」——它依赖 io_context::run() 驱动,没运行就什么都不会发生;也没内置线程安全,多个线程调用 run() 时要加 strand 或自己同步。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











