协程定时器必须分离awaiter的handle与堆上state以避免析构,使用steady_clock防止系统时间跳变,调度器用优先队列+sleep_until实现event loop,delay应返回可直接co_await的proxy对象。

协程调度器必须自己管理 awaiter 生命周期
直接用 std::suspend_always 或裸 std::coroutine_handle 会导致定时任务在超时前被析构——因为默认 awaiter 是栈对象,协程挂起后函数栈帧已销毁。必须把 awaiter 拆成两部分:可拷贝/移动的 handle(用于协程内传递),和堆上长期存活的 state(保存到期时间、回调、是否已触发等)。典型错误是写成:
struct timer_awaiter {
std::chrono::steady_clock::time_point when;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle h) {
// ❌ 错误:this 可能已被析构
schedule_task(when, [h]{ h.resume(); });
}
void await_resume() {}
};
正确做法是让 await_suspend 返回一个持久 handle(比如 std::shared_ptr 包裹的 state),并确保调度器持有该指针直到回调执行完毕。
用 std::chrono::steady_clock 而非 system_clock
系统时间可能被手动调整或 NTP 同步跳变,导致定时任务提前或永久不触发。steady_clock 是单调递增的,适合作为超时基准。注意它的单位不是秒——常见坑是写 when += 5s 却忘了 5s 是 std::chrono::seconds,而 steady_clock::time_point 的 duration 类型通常是 nanoseconds 或 microseconds,隐式转换可能丢失精度。建议统一用:
using clock = std::chrono::steady_clock;- 所有时间计算用
clock::now()+std::chrono::milliseconds(100)等显式 duration - 避免混合使用
auto推导 time_point 和 duration
调度器线程模型:单线程 event loop 最简可靠
多线程调度器需要锁或无锁队列,但绝大多数轻量级场景(如游戏 tick、网络心跳)只需一个后台线程跑 event loop。关键点:
- 用
std::priority_queue存 pending 任务,按time_point小顶堆排序 - loop 主循环:等待最近任务到期(
std::this_thread::sleep_until),然后批量执行所有已到期任务 - 插入新任务时需 notify sleep —— 用
std::condition_variable+std::mutex,但注意唤醒后要重新检查堆顶,因为可能有更早任务刚插入 - 不要在回调里直接调
co_await新定时器——那会挂起当前协程,但 event loop 线程不负责 resume 它;必须由调度器线程 resume,所以回调里应只发通知或投递到另一个协程 ready 队列
co_await 定时器的返回值设计影响使用体验
用户最常写的模式是 co_await delay(100ms),所以 delay 应返回一个可 co_await 的 proxy 对象,而不是直接返回 task<void></void>。否则会强制用户写 co_await delay(100ms).start() 这类冗余调用。推荐结构:
struct delay_awaitable {
std::chrono::steady_clock::duration d_;
delay_awaitable(std::chrono::steady_clock::duration d) : d_(d) {}
bool await_ready() const noexcept { return d_ h) {
auto when = std::chrono::steady_clock::now() + d_;
scheduler::instance().schedule(when, h); // 把 handle 注册进调度器
}
void await_resume() const noexcept {}
};
inline delay_awaitable delay(std::chrono::steady_clock::duration d) {
return delay_awaitable{d};
}
这样用户就能自然写 co_await delay(500ms),且无需额外生命周期管理。唯一要注意的是:这个 delay_awaitable 是临时对象,await_suspend 必须在它销毁前完成调度注册——所以 scheduler::schedule 内部必须立即把 h 持久化(例如存入 std::shared_ptr 包裹的 task state)。
真正难的是跨线程 resume 的安全性和 cancel 支持——如果任务在等待中被丢弃,对应的 coroutine_handle 必须能被安全取消而不 crash。这要求调度器维护一个 weak_ref 到 handle,并在 resume 前检查是否还有效。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











