不用std::chrono+std::thread直接sleep,因1000个定时器将创建1000线程,导致栈内存与调度开销压垮系统;协程用于避免线程膨胀,需单线程内可挂起/唤醒的定时任务单元。

为什么不用 std::chrono + std::thread 直接 sleep?
因为每个定时任务起一个线程,1000 个定时器就 1000 个线程,栈内存、上下文切换、调度开销直接压垮系统。协程不是为“替代线程”而生,是为“避免线程膨胀”而用——尤其在时间轮/最小堆驱动的调度场景里,你真正需要的是:单线程内可挂起、可唤醒、带状态的定时任务单元。
常见错误现象:std::this_thread::sleep_for 在协程里调用会阻塞整个协程执行器(比如 io_context),而不是只挂起当前协程;用 co_await std::chrono::seconds(5) 则根本编译不过(标准库没提供 awaiter)。
- 必须自己实现或借助第三方 awaitable 类型,如
asio::steady_timer的async_wait成员返回的可等待对象 - 底层依赖事件循环(如
boost::asio::io_context或 C++20 的std::execution兼容调度器),不能裸写协程函数就完事 - 时间精度受系统时钟和调度器 tick 间隔影响,Linux 上
CLOCK_MONOTONIC是底线,别信gettimeofday
asio::steady_timer 怎么配合协程做低开销调度?
它不是“定时器对象”,而是“可等待的超时原语”——构造时不启动,async_wait 才注册到 io_context,且返回一个符合 awaitable 要求的代理对象,天然适配 co_await。
使用场景:高频短周期心跳(如 100ms 检查连接)、延迟投递消息(如 5s 后重试)、TTL 清理(如 session 30s 过期)。
立即学习“C++免费学习笔记(深入)”;
- 每次
co_await timer.async_wait()后,必须重新调用timer.expires_after(...),否则第二次 await 会立即完成(因为过期时间仍是上次设置的) - 不要复用同一个
asio::steady_timer对象跨多个协程并发 await——内部状态非线程安全,应每个协程持有一个独立实例 - 如果任务逻辑耗时长于定时周期(比如定时每 200ms 执行,但函数跑了 300ms),需在 await 前加判断:
if (timer.expiry() ,否则会堆积
简短示例:
Huawei LiteOS是华为面向物联网领域开发的一个基于实时内核的轻量级操作系统。本项目属于华为物联网操作系统Huawei LiteOS源码,现有基础内核支持任务管理、内存管理、时间管理、通信机制、中断管理、队列管理、事件管理、定时器等操作系统基础组件,更好地支持低功耗场景,支持tickless机制,支持定时器对齐。 同时提供端云协同能力,集成了LwM2M、CoAP、mbedtls、LwIP全
auto do_scheduled_task(asio::steady_timer& timer) -> asio::awaitable<void> {
while (true) {
timer.expires_after(200ms);
co_await timer.async_wait();
// … 实际工作,注意别超时
}
}
自己实现最小堆时间轮,协程怎么挂起不卡死主线程?
时间轮或最小堆本身只是数据结构,真正让协程“按时醒来”的,是事件循环对超时事件的轮询与唤醒机制。你不能让协程自己 while 循环检查时间,那等于忙等;也不能让它 co_yield 后靠外部信号唤醒——得把“何时唤醒”这个决策权交给调度器。
关键点在于:协程挂起时,必须把自身句柄(比如一个 std::coroutine_handle)注册进时间轮的对应槽位,并绑定一个“到期回调”,该回调负责恢复协程执行。
- 回调里不能直接
handle.resume(),要通过调度器 post(如io_context.post([&] { handle.resume(); })),否则可能破坏线程安全或重入逻辑 - 协程销毁前必须从时间轮中移除,否则到期时 resume 已释放栈,必崩。建议用 RAII 封装:
struct timer_guard { ~timer_guard() { wheel.cancel(handle); } } - 最小堆 pop 最小节点后,要立刻检查堆顶是否已超时,而不是固定 sleep 1ms——高精度调度下,这个“检查间隔”就是误差上限
协程定时器在 ASIO 和 libunifex 下的行为差异
ASIO 的 steady_timer::async_wait 返回的是一个 awaitable,但它背后强依赖 io_context 的运行;libunifex 的 schedule_after 则更偏向纯函数式组合,可以脱离具体 executor 存在,但落地仍需绑定调度器。
性能影响明显:ASIO 默认用红黑树管理定时器,O(log n) 插入/删除;libunifex 的 schedule_after 在某些 executor 实现里用的是分层时间轮,O(1) 插入,但内存占用略高。
- ASIO 中若
io_context::run()未启动,所有co_await timer.async_wait()都不会触发,也不会报错,只是永远挂起 - libunifex 的
schedule_after(5s) | then([]{...}) | sync_wait()看似简单,但sync_wait会阻塞当前线程,协程优势全丢——必须搭配schedule+start_detached或自定义 executor - 两者都不支持“动态修改已注册定时器的到期时间”,只能 cancel 再 add,这点容易被忽略
复杂点从来不在协程语法,而在时间语义和生命周期对齐:什么时候注册、谁负责取消、超时回调在哪条线程执行、协程栈和定时器对象谁先析构——这些地方一错,就是 core dump 或静默丢失定时任务。







