协程任务调度器核心是std::coroutine_handle与手动状态管理,需自实现调度逻辑;最简结构为可运行队列加驱动循环,协程创建即入队,resume后须立即检查done()并销毁已完成handle。

协程任务调度器的核心是 std::coroutine_handle 和手动状态管理
标准 C++20 协程本身不提供调度器,co_await 只负责挂起/恢复,调度逻辑必须自己写。关键不是“怎么启动协程”,而是“谁来决定哪个协程该恢复、何时恢复”。最简调度器只需一个可运行任务队列 + 一个驱动循环,所有协程都以 std::coroutine_handle 形式入队。
常见错误是试图在 await_suspend 里直接调用 resume() —— 这会同步执行,失去调度意义;正确做法是把 handle 存起来,由调度器统一轮询或事件触发后恢复。
-
std::coroutine_handle必须在 resume 前保持有效:不能是栈上临时对象、不能已 destroy - 不要用裸指针存 handle,推荐
std::vector<:coroutine_handle>> </:coroutine_handle>或std::queue(注意std::queue不支持 move-only 类型,得用std::deque) - 调度器自身不能是协程,它必须是普通函数(比如
run()),否则无法启动第一个协程
如何让协程自动进入调度器队列(promise_type::get_return_object() 是入口)
每个协程函数返回一个自定义类型(如 Task),它的 promise_type 控制协程生命周期。关键是在 get_return_object() 中创建 handle 并立即入队——这样协程一创建就“就绪”,而不是等你手动 resume。
示例片段:
立即学习“C++免费学习笔记(深入)”;
struct Task {
struct promise_type {
Task get_return_object() {
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
void return_void() {}
};
std::coroutine_handle<promise_type> handle;
Task(std::coroutine_handle<promise_type> h) : handle(h) {}
~Task() { if (handle) handle.destroy(); }
};
注意:initial_suspend() 返回 std::suspend_never 才能让协程创建后立刻进入调度队列;若返回 std::suspend_always,它会挂起,你得手动 resume 一次才能入队——这违背“自动调度”初衷。
调度器 run() 循环中 resume 的安全边界在哪
每次 resume() 后必须立刻检查协程是否已完成(handle.done()),否则可能对已销毁的 handle 调用 resume,引发未定义行为。完成的协程 handle 应立即 destroy,不能留着。
- resume 前不必检查
done(),但 resume 后必须检查 - 不能在
await_suspend中 resume 当前 handle(会导致栈溢出),这是死循环陷阱 - 如果协程在
final_suspend挂起,done()返回 true,此时应 destroy handle,不能再 resume - 简单调度器可采用“运行到空”策略:
while (!ready_queue.empty()) { auto h = pop(); h.resume(); if (h.done()) h.destroy(); }
为什么不用 std::jthread 或线程池封装调度器
协程调度器本身不等于多线程——它可以完全单线程运行(比如游戏主循环、嵌入式事件循环)。加线程反而引入竞态:多个线程往同一队列 push/pop std::coroutine_handle 需要锁,而 handle 是 move-only、不可复制,锁粒度难控制。
真正需要并发调度时,更稳妥的做法是分层:单个调度器绑定一个线程,多个调度器实例各自独立运行;跨调度器通信走 channel 或原子队列。强行用 std::jthread 包裹 run() 并不解决本质问题,还掩盖了协程与线程的职责分离。
最容易被忽略的一点:协程的栈空间由编译器分配在 caller 栈帧上(默认 stackful 协程不存在),所以调度器切换协程不涉及栈拷贝,但这也意味着不能跨线程 resume 同一个 handle——除非你用 boost::context 或平台 API 自行管理栈内存。


















