用一个生产者-消费者场景,把 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

深入学习 std::span

深入学习 std::span 头文件:<span> 命名空间:std 编译器要求:C++20 起 一、设计动机:统一连续内存的访问接口 1.1 C++ 中连续内存的 N 种传参方式 在没有 span 之前,传递"一段连续内存"的方式五花八门: 1 2 3 4 5 6 7 8 9 10 11 12 13 // 方式1:C 风格——指针 + 长度(容易出错,长度可能传错) void process(const int* data, size_t len); // 方式2:模板——编译膨胀,每种容器实例化一份 template <typename Container> void process(const Container& c); // 方式3:特化 vector 引用——不接受 array 或 C 数组 void process(const std::vector<int>& v); // 方式4:迭代器对——语法啰嗦,不直观 template <typename Iter> void process(Iter begin, Iter end); 核心问题: 没有一种统一的、类型安全的方式说"我只需要一段连续内存的只读/可写视图"。 1.2 span 的解法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <span> #include <vector> #include <array> #include <cstdio> // ✅ 一个函数接受所有连续内存容器 void process(std::span<const int> data) { for (int val : data) { printf("%d ", val); } printf("\n"); } int main() { // span 能从任何连续内存容器隐式构造 std::vector<int> vec = {1, 2, 3, 4, 5}; std::array<int, 3> arr = {10, 20, 30}; int cArr[] = {100, 200, 300, 400}; process(vec); // vector → span:隐式转换 process(arr); // array → span:隐式转换 process(cArr); // C 数组 → span:隐式转换 process({vec.data() + 1, 3}); // 子区间:手动指定 {ptr, count} } 一句话总结:span 是连续内存的"通用视图"——不拥有数据、不分配内存、只是指针+长度的薄封装。 ...

May 25, 2022 · 10 min · 1921 words