直接用 std::coroutine_handle 无法构成可靠协程池,因其仅是轻量指针,无调度、队列、线程绑定或生命周期管理;需补全任务包装器、调度器和资源节制策略。

为什么直接用 std::coroutine_handle 无法构成可靠协程池
因为 c++20 无栈协程本身不附带调度语义——coroutine_handle 只是轻量指针,挂起/恢复完全由用户控制,没有内置队列、线程绑定或生命周期管理。你手动把一堆 coroutine_handle 塞进一个 vector 里,不等于有了“池”;它既不自动分发到线程,也不回收内存,更不会拒绝过载任务。
真正要落地,必须补三块:任务包装器(含状态机封装)、调度器(线程安全的等待/唤醒/分发逻辑)、资源节制策略(最大并发数、超时、取消)。
-
co_await表达式返回的awaiter必须可被多次复用或明确销毁,否则重复 resume 会 UB - 所有跨线程传递的
coroutine_handle必须用resume()而非operator()(),后者仅限同栈调用 - 协程帧(frame)默认在堆上分配,若未显式
operator new重载或使用promise_type::get_return_object_on_allocation_failure,OOM 时直接std::terminate
如何设计可复用的协程任务包装器 task<t></t>
关键不是让 task 自己跑,而是让它能被调度器识别、挂起、注入上下文并安全析构。标准做法是让 promise_type 持有状态标识(如 state_t { pending, ready, errored })和可选的 std::optional<t></t> 结果缓存。
示例中常漏掉的是:析构函数必须检查是否已 resume 或已 destroy,否则 double-destroy 协程帧会导致崩溃。
立即学习“C++免费学习笔记(深入)”;
- 在
promise_type::~promise_type()中调用if (handle) handle.destroy();是错的——应先判断!handle.done()才可能需要 cancel - 若任务被丢进池后尚未开始执行(即
handle.done() == false),需提供cancel()接口主动跳过初始 suspend point - 不要在
await_suspend里直接push_back到全局队列——要用std::atomic<bool></bool>标记是否已入队,避免多线程重复提交
线程安全的调度器怎么避免虚假唤醒和竞争丢失
典型错误是用 std::condition_variable + std::queue<coroutine_handle>></coroutine_handle>,但 notify_one() 不保证唤醒正在 wait 的线程——如果 notify 发生在 wait 前,信号就丢了。必须配合循环检查与原子标志。
推荐组合:std::atomic<size_t> ready_count</size_t> 记录待处理数量,搭配 std::mutex 保护队列 + std::condition_variable 仅作阻塞提示,每次 wait 前先读 ready_count.load()。
- worker 线程 loop 中:先
if (ready_count.load() == 0) cv.wait(lock),唤醒后立刻ready_count.fetch_sub(1)再 pop,防止多个线程同时 pop 同一 handle - 不要在
await_suspend中直接调用cv.notify_one()——应先 push 到队列,再ready_count.fetch_add(1),最后 notify - 若启用多 worker,注意
coroutine_handle的 resume 必须发生在目标线程栈上;跨线程 resume 需通过post()或std::Thread::id绑定,不能裸调resume()
协程池的硬约束:内存、栈空间和取消传播必须显式控制
无栈协程虽不占栈,但每个挂起点仍需保存局部变量副本(协程帧),且帧大小在编译期固定。一旦协程内出现大数组、std::String 或嵌套 Lambda,帧体积飙升,极易触发分配失败。
取消不是自动的:C++20 没有 co_cancel,必须靠 std::stop_token 注入 promise,并在每个 awaitable 的 await_ready() 和 await_suspend() 中轮询 stop_requested()。
- 帧过大时,
operator new分配失败会调用promise_type::get_return_object_on_allocation_failure()—— 若未定义,直接 terminate - 不要依赖 RAII 在协程作用域内释放资源:若协程被 cancel,
~promise_type()会被调用,但局部变量的析构函数不一定执行(取决于挂起位置) - 池级超时必须由调度器统一计时,而非每个 task 自己
std::chrono::steady_clock::now()—— 高频创建 task 时,时钟调用开销和精度漂移会放大误差
实际最难的从来不是启动协程,而是确保它在任何路径下(正常结束、异常退出、被取消、线程中断)都不泄漏帧、不 double-resume、不误唤醒其它协程。这些边界全得手工覆盖,没有语法糖兜底。