协程任务必须持有调度器引用以避免访问已销毁内存,正确做法是协程帧捕获shared_ptr或调度器统一管理生命周期;定时器必须用steady_clock;await_suspend需注册到epoll/kqueue而非sleep;取消机制需原子变量区分状态并严格同步。

协程任务对象必须持有调度器引用,否则 resume 时会访问已销毁内存
常见错误是把 task 对象作为局部变量返回,而调度器在函数退出后析构,后续协程恢复时调用 await_ready() 或 await_resume() 就会触发 UBSAN 报错或 segfault。正确做法是让协程帧(coroutine frame)显式捕获调度器的 shared_ptr,或由调度器统一管理任务生命周期。
实操建议:
- 定义任务类型时,构造函数强制接收
std::shared_ptr<timer_scheduler></timer_scheduler>,并存为成员变量 - 调度器内部用
std::vector<:unique_ptr>></:unique_ptr>持有所有活跃任务,避免裸指针管理 - 不要依赖协程返回值自动析构任务——C++20 协程默认不保证
destroy()被调用,尤其在未 resume 完就放弃时
定时器唤醒必须用 std::chrono::steady_clock,不能用 system_clock
系统时间可能被手动调整或 NTP 同步回拨,导致 system_clock::now() 突然跳变,使定时任务提前或永久挂起。而 steady_clock 是单调递增的,专为超时/延时设计。
实操建议:
- 所有内部计时、等待点、超时判断一律使用
std::chrono::steady_clock::time_point - 用户传入的绝对时间(如
system_clock::time_point)需先转换:steady_clock::from_time_t(system_clock::to_time_t(tp))不可靠,应改用steady_clock::now() + (tp - system_clock::now())近似换算 - Linux 下可考虑
clock_gettime(CLOCK_MONOTONIC, ...)做底层校准,但标准库封装已足够
await_suspend() 中必须注册到 epoll/kqueue 或线程池,不能只 sleep
如果仅在 await_suspend() 里调用 std::this_thread::sleep_for(),整个线程会被阻塞,协程失去“轻量级”意义;且无法响应外部取消信号或动态重调度。
实操建议:
- Linux 推荐用
timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK)配合 epoll;macOS 用kqueue+EVFILT_TIMER - 跨平台可退化为单个后台线程轮询:维护最小堆(
std::priority_queue)按到期时间排序,用condition_variable+notify_one()唤醒,但注意唤醒延迟略高 - 注册时返回
std::coroutine_handle,并在定时器就绪后调用resume(),而非重新进入调度循环
取消机制要区分「主动 cancel」和「协程已结束」两种状态
用户调用 cancel() 后,协程可能正在运行、已暂停、或已完成。直接调用 destroy() 会导致 double-free 或 use-after-free;忽略则无法及时释放资源。
实操建议:
- 任务基类加一个
std::atomic<bool> m_cancelled{false}</bool>和std::atomic<bool> m_done{false}</bool> -
await_suspend()开头检查m_cancelled.load(),若为 true 则立即返回 false(不挂起),让协程继续执行 cleanup 逻辑 -
await_resume()中检查m_cancelled并抛出std::operation_canceled异常,由用户决定是否 catch - 调度器的清理线程应在
resume()后再检查m_done,确认完成后才从容器中移除
协程调度器最难的部分不在挂起与恢复,而在状态同步——尤其是多线程环境下,cancel() 和 resume() 的竞态必须靠原子操作+内存序严格约束,漏掉 memory_order_acquire/release 就可能偶发崩溃。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











