协程延时任务不能直接用std::this_thread::sleep_for,因其会阻塞整个线程,破坏协程并发性;正确做法是通过asio::steady_timer等调度器异步等待,配合coroutine_handle实现挂起与恢复。

协程延时任务为什么不能直接用 std::this_thread::sleep_for
因为协程挂起期间要让出线程控制权,而不是阻塞整个线程。直接调用 std::this_thread::sleep_for 会冻结当前线程,破坏协程的并发意图——尤其在单线程事件循环(如 asio::io_context)中完全不可接受。
正确做法是把延时转为异步等待:注册一个定时器,到期后恢复协程。关键在于协程暂停时需保存状态,并绑定到定时器回调上。
- 使用
std::experimental::coroutine_handle或 C++20 的std::coroutine_handle暂存协程入口 - 延时逻辑必须由调度器(如
asio::steady_timer)驱动,而非线程休眠 - 避免在协程挂起点捕获局部变量地址,定时器回调可能在另一栈帧执行
如何用 asio::steady_timer 配合协程实现链式延时
libuv 或 asio 是最常用的底层支撑。以 boost::asio 为例,它提供可 await 的 async_wait,天然适配协程语义。
核心是定义一个可 await 类型,其 await_ready 返回 false,await_suspend 启动定时器并传入 coroutine_handle,await_resume 什么都不做即可。
struct delay_op {
asio::steady_timer timer;
delay_op(asio::io_context& ctx, std::chrono::milliseconds ms)
: timer(ctx, ms) {}
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle h) {
timer.async_wait([h](const asio::error_code&) { h.resume(); });
}
void await_resume() const noexcept {}
};
- 每次
co_await delay_op(ctx, 100ms)就是一次挂起+延时+恢复 - 多个
co_await串起来自然形成任务链,无需手动管理状态机 - 注意
timer必须在await_suspend中构造或已初始化,否则悬空
链式任务中如何传递数据和错误
协程函数返回类型决定能否带出值。若用 task<int></int> 这类自定义协程类型(非 void),可在 return 或 co_return 传出结果;异常则通过 co_await 抛出并被外层捕获。
但延时本身不产生值,所以数据流靠局部变量或闭包捕获维持。常见陷阱是跨延时修改 captured 变量导致竞态——尤其当协程被调度到不同线程时。
- 推荐用
std::shared_ptr包裹上下文数据,确保生命周期覆盖整个链 - 若依赖
asio::io_context::strand保证顺序,需在每个co_await前显式绑定到 strand - 不要在
await_suspend里直接操作 captured 的非原子变量
为什么不用 std::jthread + std::condition_variable 模拟协程延时
可以模拟,但不是“基于协程”的实现——它只是用线程+条件变量伪造挂起,本质仍是多线程阻塞模型。协程的价值在于用户态轻量切换和单线程高并发能力。
一旦引入真实线程等待,就失去协程的内存局部性、低开销和可预测调度特性。比如在嵌入式或游戏主线程中,额外线程唤醒延迟不可控,且增加栈内存占用。
-
std::jthread无法与co_await衔接,无法构成真正的协程链 - 每个延时都开新线程?资源爆炸;复用线程池?又回到传统异步回调地狱
- 真正协程链的关键是“挂起时不交出线程”,这点只有调度器+awaiter 能做到
实际写链式延时任务时,最容易被忽略的是定时器对象的生存期管理——它必须活到回调触发,又不能泄漏。用 std::shared_ptr 托管 steady_timer 是最稳妥的做法,哪怕多一次引用计数也比崩溃强。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











