协程世界的并发编排:从需求反推 when_all 的实现
协程世界的并发编排:从需求反推 when_all 的实现 本专栏文章:拆开 cppcoro 给你看 · 第 3 篇 这篇文章我想换个写法。前两篇是"先给答案再解释",这篇反过来——从需求出发,一步步推到最终实现。 需求很简单:三个查询并发执行,全部完成后取结果。但从这个需求到最终的 when_all,踩了三个坑:顺序等待太慢 → 手动管理太繁琐 → 注册竞态太难搞。我们逐个填。 1. 场景引入:三个并发查询 1 2 3 task<User> load_user(int id); task<Order> load_orders(int userId); task<Address> load_address(int userId); 尝试 1:顺序等待(慢) 1 2 3 4 5 6 task<void> handle(int userId) { auto user = co_await load_user(userId); // ~200ms auto orders = co_await load_orders(user.id); // ~300ms auto address = co_await load_address(user.id); // ~100ms // 总耗时: ~600ms——三个没有依赖的操作却串行执行了 } 尝试 2:手动管理 counter(繁琐) 每次需要并发时都要手动写计数器逻辑——大量重复代码,容易出错。 我们需要一个泛化的并发执行工具。这就是 when_all 的由来。 2. 核心问题:怎么知道"所有任务都完成了"? 答案就是上一篇文章提到的 when_all_counter: 1 2 3 4 class when_all_counter { std::atomic<std::size_t> m_count; std::coroutine_handle<> m_awaitingCoroutine; }; 让我们从头推导它的设计。 Step 1:计数器初始化 1 when_all_counter counter(3); // 3 个任务 Step 2:每个任务完成时递减 1 2 3 4 5 6 void notify_awaitable_completed() noexcept { if (m_count.fetch_sub(1, std::memory_order_acq_rel) == 1) { // 我是最后一个完成的 → 唤醒等待者 m_awaitingCoroutine.resume(); } } Step 3:等待者注册自己 1 2 3 4 5 6 bool try_await(std::coroutine_handle<> awaitingCoroutine) noexcept { m_awaitingCoroutine = awaitingCoroutine; return m_count.fetch_sub(1, std::memory_order_acq_rel) > 1; // >1 → 还有任务没完成 → 挂起 // ==1 → 所有任务在注册前就完成了 → 不挂起 } 为什么 m_count 初始 = 任务数 + 1? 💡 这是本文第一个关键洞察——额外的那 1 票代表"注册尚未完成"。 ...