协程任务并行需依赖外部调度器支持joinable语义,C++20协程本身不提供调度或join机制;co_await串行执行,真正并行须用when_all等接口并注意调度器绑定、结果移动安全与异常传播。

协程任务分发前必须确保调度器支持 joinable 语义
标准 C++20 协程本身不提供调度或 join 机制,co_await 只负责挂起/恢复,不隐含同步等待。若直接对多个 task<T> 对象调用 co_await,它们会串行执行——除非你手动把它们塞进一个能并发调度的 executor 中。
常见错误是误以为 co_await task1; co_await task2; 是并行的,实际是顺序等待。真正并行需依赖外部调度器(如 libunifex、cppcoro 或自定义线程池)暴露 when_all 类接口,或自己构造可等待的“聚合句柄”。
- libunifex 提供
unifex::when_all,返回一个可co_await的sender,内部自动并发启动所有子任务 - cppcoro 有
cppcoro::when_all_ready,但返回的是task<std::vector<T>>,需注意其 awaiter 在所有子任务完成前不会 resume - 手写时务必保证每个子协程绑定到同一 executor,否则可能因线程切换丢失上下文或引发 data race
如何安全合并多个协程结果(避免 dangling reference 和 move-only 类型陷阱)
合并结果最易出错的地方不是并发逻辑,而是值传递方式:co_await 返回的通常是右值引用,若直接存入 std::vector<T> 而 T 是 move-only(如 std::unique_ptr),必须显式 move;若 T 含引用成员或指向栈内存,则可能悬空。
例如:一个子协程返回 std::string&&,你在 when_all 回调里直接 push_back 到 vector,结果是未定义行为——因为 string 临时对象在 await 表达式结束时已被析构。
立即学习“C++免费学习笔记(深入)”;
- 始终用
auto result = co_await sub_task;捕获,让编译器决定是 copy 还是 move - 对 move-only 类型,显式写
results.push_back(std::move(result)); - 避免返回局部变量的引用或指针;协程函数返回类型应为
task<T>而非task<T&> - 若需共享数据,改用
std::shared_ptr<T>或std::optional<T>包装
使用 libunifex when_all 时要注意 sender 链的 completion scheduler
unifex::when_all 生成的 sender 默认以“首个子 sender 的 completion scheduler”为回调执行上下文。这意味着:如果五个子任务分别由不同线程池调度,when_all 的完成回调大概率在第一个完成任务的线程上运行——而不是你期望的主线程或某个专用 IO 线程。
这会导致意外的线程切换,尤其当后续逻辑涉及 GUI 更新、文件写入或持有 non-thread-safe 对象时,极易 crash。
- 用
unifex::with_query_value(unifex::get_scheduler, my_scheduler)强制指定 completion 调度器 - 或在
when_all后接unifex::schedule_on(my_scheduler),但注意这会引入额外调度开销 - 调试时可打印
std::this_thread::get_id()验证回调线程是否符合预期 - libunifex v2023.09+ 支持
unifex::transfer_just预绑定 scheduler,比链式调用更清晰
手写 joinable task wrapper 的最小可行结构
若不想引入第三方库,可用 std::jthread + std::promise 封装协程,但必须解决两个核心问题:生命周期管理与异常传播。标准 std::future 不支持 co_await,需自定义 awaiter。
关键点在于:awaiter 的 await_suspend 必须把协程 handle 存入 promise,并在线程完成时调用 handle.resume();而 promise 析构时要确保 handle 不被销毁后仍被 resume。
- 用
std::shared_ptr<std::promise<T>>管理 promise 生命周期,避免悬挂 - awaiter 的
await_ready()应返回false,强制挂起;不要试图优化成立即完成(除非你能 100% 确保无副作用) - 子协程内抛异常时,必须捕获并调用
promise.set_exception(std::current_exception()),否则异常会终止整个线程 - join 逻辑放在主协程里:循环
co_await每个封装后的 future,或用std::vector<awaitable>手动轮询(不推荐,效率低)
真正麻烦的从来不是怎么写协程,而是谁来保证所有子任务的栈内存、堆资源、线程亲和性、异常路径都受控。哪怕只漏掉一个 std::move 或 scheduler 绑定,上线后都可能表现为偶发崩溃或结果错乱。


















