协程世界的并发编排:从需求反推 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 票代表"注册尚未完成"。 ...

November 18, 2024 · 7 min · 1283 words

用一个生产者-消费者场景,把 cppcoro 的 7 个协程同步原语全部串起来

用一个生产者-消费者场景,把 cppcoro 的 7 个协程同步原语全部串起来 本专栏文章:拆开 cppcoro 给你看 · 第 2 篇 上一篇文章我把 cppcoro 的 task<T> 拆成了 5 个版本迭代。今天要搞定协程世界里另一个核心问题:多个协程之间怎么同步? 市面上的做法是一张 API 列表——“这是 async_mutex,这是 async_latch,啥时候用自己看着办”。我不这么讲。本文用同一个生产者-消费者场景,从简单到复杂逐步升级,每级只比上一级多一个特性,一共串起 7 个同步原语: 1 2 3 4 5 6 7 8 9 场景:生产者协程产生数据,消费者协程处理数据 Level 1: 单消费者,发信号通知 → single_consumer_event Level 2: 同上,但信号自动复位 → single_consumer_async_auto_reset_event Level 3: 多消费者,手动复位 → async_manual_reset_event Level 4: 多消费者,自动复位 → async_auto_reset_event Level 5: 共享数据的互斥访问 → async_mutex Level 6: 等待 N 个操作完成 → async_latch Bonus: when_all 的核心组件 → when_all_counter 每级都是可运行的代码。 ...

November 15, 2024 · 7 min · 1460 words

cppcoro 的 task\<T\> 到底做了什么?从 25 行到 90 行,5 版迭代拆开看

cppcoro 的 task<T> 到底做了什么?从 25 行到 90 行,5 版迭代拆开看 本专栏文章:拆开 cppcoro 给你看 · 第 1 篇 上一篇我们手写了 30 行的 Generator,理解了协程帧、co_await 的展开步骤和 promise_type 的 7 个接口。今天来真的——把 cppcoro 的 task<T> 拆开看。 cppcoro 的 task.hpp 一共 482 行,但你不需要直接从第 1 行读到第 482 行。其中差不多 300 行是关于对称转移/非对称转移的条件编译、MSVC 的 workaround、异常处理细节。真正核心的东西不到 100 行。 本文的学习策略是分 5 版迭代——每个版本只加一个功能点,每版都是可编译运行的完整代码。 版本 功能 行数 v1 只能 co_return,不能 co_await ~25 v2 加上 co_await 支持(用 sync_wait 驱动) ~40 v3 加上对称转移(final_awaitable) ~55 v4 加上异常处理 ~70 v5 完整版——对应 cppcoro 源码 ~90 v1:只能 co_return 的 task v1 的目标很简单:理解一个协程怎么把结果从协程帧传回调用者。 ...

November 10, 2024 · 9 min · 1732 words

从 0 到 1 理解 C++ 协程:30 行代码搞定协程帧、co_await 和 promise_type

从 0 到 1 理解 C++ 协程:30 行代码搞定协程帧、co_await 和 promise_type 本专栏文章:拆开 cppcoro 给你看 · 第 0 篇 如果你写过 C++ 异步代码,一定踩过回调地狱的坑——3 层嵌套是起步,5 层不稀奇。C++20 的协程本该解决这个问题,但市面上的入门材料不是太浅(“协程就是可以暂停的函数”——然后呢?),就是太深(上来就讲对称转移和 IOCP),中间缺了一环。 这篇文章补上这一环。读完之后你不需要看任何其他"协程入门"材料,可以直接开始拆 cppcoro 的 task<T> 源码。 1. 协程到底解决了什么问题? 1.1 先看一段让你血压升高的代码 假设你要从数据库查用户、再查订单、最后发网络请求——三个操作都是异步的。传统回调写法长这样: 1 2 3 4 5 6 7 8 9 10 11 12 // 回调地狱(callback hell) void handle_request(int userId) { db.query_user(userId, [](User user) { // 回调1 db.query_orders(user.id, [](Orders orders) { // 回调2 http.send(orders, [](Response rsp) { // 回调3 // 三层嵌套...实际项目里可能是五六层 // 错误处理散落各处,上下文信息全部丢失 finalize(rsp); }); }); }); } 你发现没有——代码逻辑明明是线性的(查用户 → 查订单 → 发请求),但写出来是嵌套的。三层还算友好,加到五六层的时候,你根本不想看自己的代码。 ...

November 8, 2024 · 7 min · 1457 words