c++oding="utf-8" ?>
std::coroutine_handle不能直接用作调度器,因其仅是指向协程帧的裸指针,缺乏生命周期管理、队列、线程安全及异常传播能力;裸调resume()极易引发崩溃。

直接用 std::coroutine_handle 调度会崩溃,不是你写错了,是它根本不带生命周期管理、队列、线程安全或异常传播能力——裸调 resume() 等于在悬崖边踩油门。
为什么不能把 coroutine_handle 当调度器用
它只是一个指向协程帧的指针,没有“该不该跑”“谁来唤醒”“挂起后放哪”这些逻辑。常见崩法包括:
-
segmentation fault:协程已destroy(),你还resume() -
pure virtual method called:promise 对象被析构,但 handle 还在队列里 - 悬垂 handle:把
coroutine_handle存在栈上,跨co_await后访问失效地址
实操建议:
- 所有
resume()前必须检查h && !h.done() - handle 必须堆分配或绑定到 promise 对象生命周期内(比如存为
promise_type::handle成员) - 单线程调度器就别碰锁;多线程必须用无锁队列(如
moodycamel::ConcurrentQueue),不能裸 push/popstd::queue
promise_type 里必须显式保存 handle
编译器生成的协程帧里,promise_type 实例和 coroutine_handle 是绑定的,但 handle 不会自动暴露给外部——你得自己存一份,否则 final_suspend 或自定义 awaiter 里根本拿不到它。
错误写法:get_return_object() 只返回临时 Task,没把 handle 挂到 promise 上。
正确写法:
struct Task {
struct promise_type {
std::coroutine_handle<promise_type> handle;
Task get_return_object() {
handle = std::coroutine_handle<promise_type>::from_promise(*this);
return Task{handle};
}
// …
};
};
</promise_type></promise_type>
关键点:
-
handle成员必须是public,且在get_return_object()中立即赋值 - 不能等
initial_suspend返回后再取 handle——那时帧可能还没完全初始化 - 若需从 awaiter 访问 handle,必须通过
handle.promise()反查,而反查的前提就是 handle 已存好
await_suspend 怎么把协程交出去
这是协程挂起时唯一可控的“注册入口”。你写的 awaiter 在 await_suspend() 里拿到 handle,然后扔进调度器队列——不是靠它“暂停”,而是靠它“移交控制权”。
典型错误:
- 返回
false却不安排 resume:协程直接继续执行,跳过挂起语义 - 在
await_suspend()里直接h.resume():导致重入、栈溢出或死循环 - 复用同一个
asio::steady_timer对象并发 await:内部状态非线程安全,行为未定义
实操建议:
- 返回
void:表示你已把 handle 推入就绪/延时队列,协程立刻挂起 - 返回
bool:只在极少数需要延迟决定时用(如条件唤醒),且true表示已安排 resume,false表示不挂起 - 延时任务必须每个协程配独立
asio::steady_timer实例,并在每次co_await前调timer.expires_after(...)
就绪队列选 deque 而不是 queue
std::queue 底层默认用 std::deque,但某些 STL 实现(如 libstdc++ 旧版)在 move 构造时会破坏 coroutine_handle 内部指针——因为 handle 不是 trivially movable,而 queue 的 pop 操作隐含 move。
更稳的选择是直接用 std::deque<:coroutine_handle>> </:coroutine_handle>,并手动 front/pop:
std::deque<:coroutine_handle>> ready_queue;
void schedule(std::coroutine_handle h) { ready_queue.push_back(h); }
void run() {
while (!ready_queue.empty()) {
auto h = ready_queue.front();
ready_queue.pop_front();
if (h && !h.done()) h.resume();
}
}
</:coroutine_handle>
注意点:
- 不要用
std::queue包一层再 exposecontainer——接口不透明,隐患难排查 - pop 前必须判空,resume 前必须判
h && !h.done(),两者缺一不可 - 如果调度器要支持休眠队列(定时任务),得用最小堆(
std::priority_queue)按超时时间排序,每次 run 前检查堆顶是否到期
真正难的不是切换,是让每个协程函数都意识到:“我随时会被切走”——局部变量地址无效、std::string 析构时机不确定、std::thread_local 完全不可见。这些不是语法错误,是运行时幽灵,调试器看不见,ASan 抓不住,只能靠设计约束提前堵死。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











