不能直接用 std::this_thread::sleep_for 在协程里等待,因为它是同步阻塞调用,会使整个线程停住,导致协程调度器卡死;正确做法是使用异步定时器(如 boost::asio::steady_timer)注册超时事件、挂起协程,由调度器到期唤醒。

为什么不能直接用 std::this_thread::sleep_for 在协程里“等”
协程挂起时不会阻塞线程,但 std::this_thread::sleep_for 是同步阻塞调用——它会让整个线程停住,协程调度器就卡死。你写了个 co_await,结果发现其他协程全不跑了,就是这个原因。
真正可行的做法是把延时交给异步调度器(比如基于 std::chrono + std::condition_variable 或系统级 timerfd / IO 多路复用),然后让协程挂起、等待调度器唤醒。
- 别在协程里调用
sleep_for、usleep或任何同步等待函数 - 所有延时必须转为「注册超时事件 → 挂起协程 → 由调度器在到期时恢复」
- 如果你没自己实现调度器,优先考虑
boost::asio::steady_timer或libuv这类已验证的异步 timer
用 boost::asio::steady_timer 实现可等待的延时协程
Boost.Asio 提供了原生支持协程的 timer,底层用 epoll(Linux)或 IOCP(Windows)驱动,不会阻塞线程。关键在于它返回一个 awaitable 对象。
示例:等待 500ms 后继续执行
auto delay = [](int ms) -> boost::asio::awaitable<void> {
boost::asio::steady_timer timer(co_await boost::asio::this_coro::executor);
timer.expires_after(std::chrono::milliseconds(ms));
co_await timer.async_wait(boost::asio::use_awaitable);
};</void>
-
co_await timer.async_wait(...)是真正的非阻塞挂起,调度器会在到期后 resume 协程 - 必须确保
boost::asio::io_context正在运行(调用io_context.run()或io_context.poll_one()) - 注意
expires_after和expires_at的区别:前者相对当前时间,后者需传入time_point - 如果 timer 被 cancel(比如协程被销毁前),
async_wait会以boost::asio::error::operation_aborted完成,需捕获处理
手写简易 timer awaiter 的核心陷阱
自己实现 timer awaiter 看似简单,但容易掉进三个坑:时间精度丢失、竞态唤醒、协程句柄泄漏。
- 不要用
std::this_thread::sleep_until轮询判断时间——这浪费 CPU,且无法响应 cancel - 挂起前必须原子记录协程句柄(
std::coroutine_handle),并在超时/取消时安全调用.resume()或.destroy() - 多个 timer 共享同一计时线程时,必须用互斥锁保护待触发队列,否则
push/pop可能错乱 - 如果使用
std::condition_variable+std::mutex,注意wait_until返回false表示超时,true表示被 notify —— 别反了逻辑
延时任务和普通协程的生命周期管理差异
普通协程挂起后,只要句柄还在,就能 resume;但延时协程一旦注册到 timer,就必须处理「协程提前销毁」的情况——否则 timer 到期后尝试 resume 已释放的栈,就是 UAF。
- 推荐模式:timer awaiter 内部持有
std::shared_ptr包裹的控制块,协程帧和 timer 共享该块的生命周期 - cancel 接口应标记「已取消」并清空句柄,避免 double resume
- 如果用
co_await延时后接着访问局部变量,要确认这些变量是否仍在栈上——协程挂起期间栈可能已被回收(取决于编译器优化和协程帧分配方式) - 调试时留意
coroutine_handle::done()返回值,它比!handle更可靠地反映是否已 destroy
最常被忽略的是:timer 不是协程,它只是个触发器;真正决定任务是否还有效的,是你对协程生命周期的显式管理。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











