协程挂起必须返回std::suspend_always或std::suspend_never,不可裸写co_await;push()需返回可等待类型,pop()应挂起而非阻塞,析构时须清理挂起协程以防terminate。

协程挂起时必须返回 std::suspend_always 或 std::suspend_never,不能裸写 co_await
很多初学者在实现任务队列的 await_transform 时直接写 co_await queue.push(task),结果编译失败——因为 push() 返回的是 void 或 bool,不满足可等待类型(awaitable)要求。协程挂起的唯一合法路径是通过 await_ready() 返回 false,再由 await_suspend() 注册恢复逻辑,最终靠 await_resume() 返回值。
正确做法是把任务入队和挂起解耦:入队操作同步完成,挂起由一个轻量级 awaitable 控制。常见模式是返回一个封装了 std::suspend_always 的临时对象:
struct push_awaitable {
task_t task;
async_queue* q;
<pre class="brush:php;toolbar:false;">bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) noexcept {
q->enqueue(task, h); // 入队 + 记录恢复句柄
}
void await_resume() const noexcept {}};
注意:await_suspend() 接收的 std::coroutine_handle 必须被安全持有(如存入队列节点),否则恢复时调用已销毁的 handle 会崩溃。
立即学习“C++免费学习笔记(深入)”;
await_suspend() 返回 void、bool 或 std::coroutine_handle 的行为差异
这是最容易引发未定义行为的点。返回类型决定了协程控制流如何交接:
-
void:挂起后必须由外部显式调用resume(),适合手动调度场景; -
bool:返回true表示已自行恢复(协程继续执行),false表示挂起,需后续 resume; -
std::coroutine_handle:协程立即转移控制权给该 handle,常用于链式调度(比如 A 挂起后直接跳转到 B 执行)。
在任务队列中,99% 的情况应返回 void 或 bool。若错误返回了新 handle(比如误传 std::noop_coroutine()),会导致协程栈错乱或静默跳过后续代码。
典型错误示例:await_suspend 中写了 return std::coroutine_handle::from_address(nullptr); —— 这不是“不恢复”,而是向随机内存地址跳转。
任务队列的 pop() 不应阻塞线程,而应让调用方协程挂起
同步队列的 pop() 通常用 std::mutex + std::condition_variable 等待,但协程版必须避免线程阻塞。正确方式是让消费者协程挂起,并在生产者 push() 后唤醒第一个等待者。
关键设计点:
- 队列内部维护一个等待 handle 链表(
std::deque<:coroutine_handle>> </:coroutine_handle>或无锁结构); -
pop()的 awaitable 在await_ready()中检查队列是否非空,空则挂起; -
push()成功后,从等待链表取一个 handle 并resume(); - 必须处理竞争:比如两个
pop()同时发现队列为空,都挂起,此时一个push()应只唤醒一个,避免“惊群”。
遗漏竞争处理的典型现象是:任务明明已入队,但某个 co_await queue.pop() 永远不返回。
协程销毁时未清理挂起状态会导致 std::terminate
如果一个协程被销毁(比如其 promise 对象析构)时仍处于挂起状态且没有被 resume 或 destroy,C++20 标准规定必须调用 std::terminate()。
在任务队列中,这常发生在以下场景:
- 消费者协程被 co_return 或异常退出,但还在等待 pop;
- 生产者 push 后崩溃,未触发唤醒;
- 队列本身被销毁,但仍有挂起的协程 handle 未被清除。
解决方案是在队列析构函数中遍历所有等待 handle,对每个调用 handle.destroy()(而非 resume()),并在 promise 的 unhandled_exception() 和 return_void() 中主动取消挂起逻辑。更稳妥的做法是:所有 awaitable 都持有一个 weak_ptr 到队列,挂起前检查队列是否还存活。
这个崩溃不会报具体行号,只显示 terminate called without an active exception,排查时容易误判为内存破坏。


















