Boost.Asio学习(5):c++的协程

协程是什么?

协程就是可以“暂停”和“继续”的函数,像在函数里打个断点,然后以后可以从断点继续运行,而不是重新开始。

线程 vs 协程:类比

想象你在写小说:

  • 线程

    • 你开了 3 个作者(线程),每个人都在写书。

    • 系统(OS)来回切换谁在写。

    • 切换的时候要收拾桌子、换椅子、拿走稿纸(上下文切换),开销大。

  • 协程

    • 你只有 1 个作者,但可以写一会儿暂停,去做别的,再回来接着写。

    • 暂停的时候,只需在稿纸上插个书签(挂起点),下次从书签继续,开销极小。

特性线程协程
切换方式抢占式(OS决定)协作式(程序决定)
切换成本(要保存寄存器、堆栈)(只是保存协程状态)
并发多线程可以真正并行协程必须在同一个线程轮流跑
控制权OS 控制程序员控制(通过 co_await
  • 协程 ≠ 线程,它只是单线程内的“任务切换”机制。

  • 协程可以跑在一个线程上,也可以结合线程池,让多个线程各自跑多个协程 → 混合使用

  • 协程不提供并行(不会用多个 CPU 核心),但可以轻松实现异步(不会阻塞线程)。

协程解决了什么问题?

传统异步写法(回调):

async_read(socket, buf, [](auto ec, auto size) {async_write(socket, buf, [](auto ec, auto size) {// 回调地狱});
});

协程写法:

co_await async_read(socket, buf);
co_await async_write(socket, buf);

为什么开销小?

  • 线程切换:需要保存 CPU 寄存器、切换内核态、切换栈,可能上百纳秒甚至微秒级。

  • 协程切换:只保存协程状态(程序计数器 + 局部变量),在用户态完成,纳秒级。

使用

  • C++20 正式引入协程机制,底层通过 编译器转换 + 栈帧对象(promise) 实现。

  • 三个关键字:

    • co_await —— 挂起并等待某个任务。

    • co_yield —— 挂起并返回一个值(类似生成器)。

    • co_return —— 返回最终值并结束协程。

协程的运行方式

协程不是线程,也不是普通函数,而是 编译器把协程函数转换成状态机,并返回一个“协程句柄”。

流程:

  1. 编译器检测函数是否包含 co_await/co_yield/co_return

  2. 如果包含,编译器会生成:

    • 协程状态对象(包含局部变量、挂起点、promise)。

    • 协程句柄std::coroutine_handle<>)控制 resume/destroy。

  3. 第一次调用协程函数

    • 返回一个 awaitable 对象,不会立刻执行全部逻辑。

  4. 调用 resume()

    • 协程运行到 co_await 挂起。

  5. 再次 resume()

    • 从上次挂起点继续。

关键字作用
co_return返回最终结果并结束协程
co_yield产出一个值并挂起(生成器)
co_await挂起并等待某个操作完成

常见协程返回类型

  • std::generator<T>(C++23 提供)

  • 自定义 task<T>

  • 第三方库:

    • cppcoro(最常用)

    • Boost.Coroutine

 协程常见写法

(1)最简单的生成器(C++23)

#include <coroutine>
#include <iostream>
#include <generator>std::generator<int> numbers() {for (int i = 0; i < 5; ++i)co_yield i;
}int main() {for (auto v : numbers())std::cout << v << "\n";
}

(2)异步任务(自定义 Task)

#include <coroutine>
#include <iostream>// 1. 定义返回类型 Task
struct Task {// 2. promise_type:编译器需要它来生成协程对象struct promise_type {// 编译器调用:创建协程返回值Task get_return_object() {return Task{std::coroutine_handle<promise_type>::from_promise(*this)};}// 协程开始前是否挂起?std::suspend_never initial_suspend() noexcept { return {}; }// 协程结束后是否挂起?std::suspend_never final_suspend() noexcept { return {}; }// 处理 co_returnvoid return_void() {}void unhandled_exception() {} // 异常处理};std::coroutine_handle<promise_type> handle;Task(std::coroutine_handle<promise_type> h): handle(h) {}~Task() { if(handle) handle.destroy(); }
};// 3. 协程函数:编译器自动用 Task::promise_type 来管理
Task myCoroutine() {std::cout << "Hello\n";co_await std::suspend_always{}; // 挂起协程std::cout << "World\n";
}int main() {auto t = myCoroutine(); // 创建协程,执行到 co_await 挂起std::cout << "Resume...\n";t.handle.resume(); // 恢复协程,继续执行
}

自定义 Task详细解析

为什么要定义 promise_type

  • C++20 协程和普通函数完全不同,编译器在看到协程时不会直接返回值,而是生成一个状态机对象(Frame)

  • 协程的返回类型必须定义一个 嵌套类型 promise_type

  • 编译器规则

    • 当你写:

Task myCoroutine() {co_await something;co_return;
}

编译器会查找 Task::promise_type,并生成代码去:

  1. 创建一个 promise_type 对象。

  2. 调用它的 get_return_object(),把结果作为协程的返回值。

  3. 调用 initial_suspend() / final_suspend() 控制是否立即挂起。

  4. 调用 return_void()return_value() 处理 co_return

所以:

  • promise_type 就是协程的“大脑”,控制协程生命周期。

  • 它必须提供一组固定接口(编译器调用)。

initial_suspend和final_suspend       

  • initial_suspend():协程刚创建时是否挂起?
    • 返回 std::suspend_always挂起,调用者必须手动 resume()

    • 返回 std::suspend_never不挂起,协程立即执行。

  • final_suspend():协程结束后是否挂起?
    • 一般返回 std::suspend_always,让调用者决定何时销毁协程。

  • 为什么要控制挂起?

    • 这关系到执行模型(立即运行 or 手动恢复)。

std::coroutine_handle是干啥的

  • 它是一个 轻量级句柄,指向协程帧(frame)。

  • 协程帧包含:

    • 状态机(状态编号)

    • 局部变量

    • 一个 promise_type 对象(由返回类型的 promise_type 定义)

  • std::coroutine_handle 提供操作协程生命周期的方法

    • 恢复协程(resume()

    • 销毁协程(destroy()

    • 判断是否执行完(done()

本质:它类似一个 void*,但知道协程帧布局,可以安全操作 promise 对象。

重点:协程不是线程,编译器不会自动跑完,你必须显式调用 resume(),所以要有句柄。

为什么模板参数是 promise_type

  • 协程帧里有一个 promise_type 成员。

  • coroutine_handle<promise_type> 是一个类型安全的 handle,可以直接访问 promise 对象:

handle.promise()  // 返回对 promise 对象的引用

 这就是为什么你的 Task 返回类型必须定义 promise_type,编译器才能生成 coroutine_handle<promise_type>

如果不关心 promise,可以用 无模板版本

std::coroutine_handle<> // 泛型 handle,不提供 promise()。

类成员函数 

1. resume()

  • 恢复协程执行。

  • 如果协程在挂起状态,继续执行,直到下一个挂起点或结束。

  • 所以resume()的同步地、阻塞的

2. done()

  • 判断协程是否执行完毕(到达 final_suspend() 之后)。done() == true 表示协程执行到 final_suspend()

3. destroy()

  • 销毁协程帧(释放内存),必须在协程完全结束后调用,否则 UB。

4. promise()

  • 返回 promise_type&,可以访问自定义逻辑,比如取值、状态。

举例按流程详细解释

代码流程:

#include <coroutine>
#include <iostream>// 1. 协程返回类型 Task
struct Task {struct promise_type {// 编译器会调用此函数获取协程返回对象Task get_return_object() {std::cout << "[promise_type] get_return_object()\n";return Task{ std::coroutine_handle<promise_type>::from_promise(*this) };}std::suspend_always initial_suspend() noexcept {std::cout << "[promise_type] initial_suspend()\n";return {}; // 创建后立即挂起}std::suspend_always final_suspend() noexcept {std::cout << "[promise_type] final_suspend()\n";return {}; // 协程结束后挂起,等外部销毁}void return_void() { std::cout << "[promise_type] return_void()\n"; }void unhandled_exception() { std::terminate(); }};std::coroutine_handle<promise_type> handle;explicit Task(std::coroutine_handle<promise_type> h): handle(h) {}~Task() {if (handle) {std::cout << "[Task] destroy coroutine\n";handle.destroy(); // 销毁协程 frame}}void resume() {std::cout << "[Task] resume()\n";handle.resume(); // 恢复协程}
};// 2. 协程函数:执行顺序受 suspend 控制
Task myCoroutine() {std::cout << "[Coroutine] Start\n";co_await std::suspend_always{}; // 挂起std::cout << "[Coroutine] After co_await\n";co_return;
}int main() {auto t = myCoroutine();  // 创建协程(不会立刻跑完整个函数)std::cout << "[main] first resume\n";t.resume();              // 执行到 co_await 挂起std::cout << "[main] second resume\n";t.resume();              // 执行到 co_return,final_suspend 挂起
}

输出:

[promise_type] get_return_object()
[promise_type] initial_suspend()
[main] first resume
[Task] resume()
[Coroutine] Start
[Coroutine] After co_await
[promise_type] return_void()
[promise_type] final_suspend()
[main] second resume
[Task] destroy coroutine

执行过程详细解读:

Step 1: 调用 myCoroutine()
  • 编译器看到 co_awaitmyCoroutine 改造成协程,不直接返回 Task,而是:

    1. 编译器硬编码规则

      1. 如果协程返回类型是 R,就去找 R::promise_type

      2. 通过 R::promise_type 生成一个 promise_type 类的对象promise。

    2. 调用 get_return_object() → 返回 Task,并绑定 coroutine_handle

      • std::coroutine_handle<promise_type>::from_promise(*this):把 promise 和 handle 关联。

    3. 调用 initial_suspend() → 返回 suspend_always,协程挂起,不执行函数体。

  • 此时:协程 frame(状态机)在堆上创建,但没开始执行

对象状态

  • Task:持有 coroutine_handle

  • promise_type:存储协程状态。

  • handle:指向协程 frame,能控制 resume()

Step 2: t.resume()
  • 调用 handle.resume()

  • 协程开始执行,打印 [Coroutine] Start

  • 遇到 co_await std::suspend_always{} → 立即挂起。

    • 编译器调用 await_suspend()(内部由 suspend_always 实现)。

  • 控制权返回 main,协程停在 co_await 这一行。

Step 3: 第二次 t.resume()
  • 协程从 co_await 后恢复。

  • 打印 [Coroutine] After co_await

  • 执行 co_return → 编译器调用 promise_type.return_void()

  • 协程执行到尾,进入 final_suspend() → 返回 suspend_always,协程再次挂起,等待销毁。

Step 4: 退出 main()Task 析构
  • 调用 handle.destroy() → 销毁协程 frame 和 promise_type

  • 协程彻底结束

关键点汇总

  • promise_type 是协程的“控制器”,编译器强制要求它有:

    • get_return_object()(返回 Task)

    • initial_suspend() / final_suspend()

    • return_void() / return_value()

  • coroutine_handle 是“遥控器”,用来 resume()destroy()

  • co_await 触发挂起点

  • co_return 结束协程

  • 状态机 + Frame 在堆上管理生命周期

co_yield使用详细解释

编译器在看到 co_yield expr; 时,会将它转换成 promise_type.yield_value(expr) 的调用,并把返回值当作 co_await 的对象。

也就是说:

co_yield value;

等价于:

co_await promise.yield_value(value);

所以:

  • co_yield 会调用 promise_type::yield_value(T value)

  • 这个函数必须返回一个 Awaitable 对象(通常是 std::suspend_alwaysstd::suspend_never)。

  • 编译器会像 co_await 一样调用:

    • await_ready() → 决定是否挂起。

    • await_suspend() → 挂起时的动作。

    • await_resume() → 恢复后取值。

举个完整例子:生成器

#include <coroutine>
#include <iostream>struct Generator {struct promise_type {int current_value;Generator get_return_object() {return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};}std::suspend_always initial_suspend() noexcept { return {}; }std::suspend_always final_suspend() noexcept { return {}; }void return_void() {}void unhandled_exception() { std::terminate(); }// **核心:co_yield 调用这个**std::suspend_always yield_value(int value) noexcept {std::cout << "[promise_type] yield_value(" << value << ")\n";current_value = value;return {}; // 挂起}};std::coroutine_handle<promise_type> handle;explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}~Generator() { if (handle) handle.destroy(); }bool move_next() {if (!handle.done()) {handle.resume();}return !handle.done();}int current_value() { return handle.promise().current_value; }
};// 协程函数
Generator numbers() {for (int i = 1; i <= 3; ++i) {co_yield i; // 调用 yield_value(i)}
}int main() {auto gen = numbers();while (gen.move_next()) {std::cout << "[main] got: " << gen.current_value() << "\n";}
}

执行流程

[promise_type] yield_value(1)
[main] got: 1
[promise_type] yield_value(2)
[main] got: 2
[promise_type] yield_value(3)
[main] got: 3

执行到 co_yield 时发生了什么?

  • 编译器调用 promise_type.yield_value(i)

  • 这个函数保存值(current_value = i),返回 std::suspend_always

  • 协程挂起(await_suspend())。

  • 控制权返回 main(),用户取值。

  • 下一次 resume(),协程从 co_yield 之后继续。

return_value用法

co_return 在 C++20 协程中不仅可以 co_return;,还可以 co_return value;,对应 promise_typereturn_value() 方法(或 return_void())。

  • co_return 用于在协程函数里 返回结果结束协程

  • 两种形式:

    1. co_return;:无返回值(void 协程)。

    2. co_return expr;:返回一个值。

编译器会把 co_return 翻译成 调用 promise 对象的方法

  • co_return;promise.return_void();

  • co_return expr;promise.return_value(expr);

例子:

#include <coroutine>
#include <iostream>
#include <optional>struct Task {struct promise_type {std::optional<int> value;Task get_return_object() {return Task{ std::coroutine_handle<promise_type>::from_promise(*this) };}std::suspend_always initial_suspend() noexcept { return {}; }std::suspend_always final_suspend() noexcept { return {}; }void return_value(int v) { value = v; }  // 支持 co_return v;void unhandled_exception() { std::terminate(); }};std::coroutine_handle<promise_type> h;explicit Task(std::coroutine_handle<promise_type> h) : h(h) {}~Task() { if (h) h.destroy(); }int get() {h.resume();return *h.promise().value;}
};Task compute() {std::cout << "Computing...\n";co_return 42;  // 等价于 promise.return_value(42);
}int main() {Task t = compute();std::cout << "Result = " << t.get() << "\n";
}

协程函数不会像普通函数那样直接“返回值”给调用者,而是:

  • 立即返回一个包装了 coroutine_handle 的对象(例如 Task

  • 真正的“结果”要么通过:

    • promise_type 存储(由 co_return 设置值),

    • 或由外部代码通过 handle 或包装类(如 Task::get())访问。

这个std::optional<T>又是个啥?

std::optional<T> 是 C++17 引入的一个标准库模板类,用来表示 “一个可能有值,也可能没有值的对象”

在协程里,它常用来延迟存储返回值,因为:

  • 协程一开始不会立即有返回值(因为可能挂起)。

  • 但最终会 co_return value;,所以我们需要一个容器,能先处于“空”状态,等有值再存进去。

std::optional<T> 基本概念

  • 它是一个模板类,可以包含类型 T 的值,或者为空。

  • 类似于 T*(指针),但更安全,因为不会出现悬空指针,也不需要动态分配。

  • 常用接口:

std::optional<int> opt;    // 默认无值
opt.has_value();           // 判断是否有值
opt = 42;                  // 赋值
int x = *opt;              // 取值(必须有值,否则未定义)

为什么用 optional 而不是直接 int?

  • 因为协程返回 Task<int>,在协程挂起时我们没有值。

  • 如果直接用 int value;,我们没法区分“值是 0”还是“值还没准备好”。

  • std::optional<int> 可以:

    • 初始状态 nullopt(空)。

    • return_value(42) 时赋值。

    • 之后通过 *optionaloptional.value() 取值。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/94219.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/94219.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux 中,命令查看系统版本和内核信息

在 Linux 中&#xff0c;可以通过以下命令查看系统版本和内核信息&#xff1a;1. 查看内核版本uname -a或精简显示&#xff1a;uname -r # 只显示内核版本示例输出&#xff1a;Linux ubuntu 5.4.0-135-generic #152-Ubuntu SMP Tue Nov 15 08:12:21 UTC 2022 x86_64 x86_64 x8…

数据结构总纲以及单向链表详解:

以下是基于笔记更详细的知识梳理&#xff0c;从概念到细节逐层拆解&#xff0c;帮你吃透数据结构核心要点&#xff1a; 数据结构部分的重点内容&#xff1a;一、数据结构基础框架 &#xff08;一&#xff09;逻辑结构&#xff08;关注元素间“逻辑关系”&#xff09; 笔记里提到…

模型学习系列之参数

背景 “GLM-4.5拥有 3550 亿总参数量&#xff0c;其中 320 亿活跃参数&#xff1b;GLM-4.5-Air 采用更紧凑的设计&#xff0c;拥有 1060 亿总参数量&#xff0c;其中 120 亿活跃参数。” 定义与关系 总参数量&#xff1a;模型中所有可训练参数的总和&#xff08;包括嵌入层、注…

[创业之路-535]:软件需要原型验证、产品需要原型验证、商业模式也需要原型验证

原型验证在软件、产品开发以及商业模式探索中均扮演着至关重要的角色&#xff0c;它通过低成本、快速迭代的方式&#xff0c;帮助团队验证核心假设、降低风险并优化方案。以下是针对这三个领域的具体分析&#xff1a;一、软件原型验证&#xff1a;从概念到可交互的模型核心目的…

sublime text2配置

sublime text2配置背景配置其他背景 之前下载了就把它当记事本在使用。但是&#xff0c;在使用过程中&#xff0c;有些场景很痛苦。如果说找一个字符串中的某一部分&#xff0c;虽然它通过了这个功能&#xff0c;但是不够明显&#xff0c;看瞎了。。。 配置 下面是我改的一些选…

本地通信的选择:为什么组播比广播更适合多进程协作?

零、深入解析Linux本地通信机制,对比广播与组播的核心差异 本地组播能让多进程收到消息,而本地广播不行,核心原因在于两者的设计目标、网络协议处理逻辑以及内核转发机制存在本质差异。具体可以从以下几个角度理解: 1. 通信模式与目标地址的本质区别 组播(Multicast):…

7-Django项目实战[user]-发送邮件激活账号

1.前期准备&#xff08;以QQ邮箱为例&#xff09; 登录QQ邮箱 获取授权码 2.settings.py文件配置 1&#xff09;缓存配置 # 配置缓存 CACHES {# 邮件激活随机数"default": {"BACKEND": "django_redis.cache.RedisCache","LOCATION&q…

社群团购市场选择与开源技术赋能下的下沉市场开拓策略研究——以开源AI智能名片、链动2+1模式与S2B2C商城小程序为例

摘要&#xff1a;在社群团购行业面临流量成本攀升与同质化竞争的背景下&#xff0c;下沉市场因其庞大用户基数与未被充分满足的消费需求&#xff0c;成为创业者突破增长瓶颈的关键赛道。本文以拼多多成功开拓小城镇与农村市场的案例为切入点&#xff0c;结合开源AI智能名片、链…

Ollama前端:open-webui

github&#xff1a;https://github.com/open-webui/open-webui 官网&#xff1a;&#x1f3e1; Home | Open WebUI 1、docker安装&#xff08;GPU&#xff09;&#xff1a; docker run -d -p 3000:8080 --gpusall -v ollama:/root/.ollama -v open-webui:/app/backend/data …

LeetCode513:找树最左下角的值(bfs+dfs)

文章目录一、 题目描述解法一&#xff1a;层序遍历 (BFS) - 最直观的解法核心思路代码实现优缺点分析解法二&#xff1a;递归 (DFS) - 更深度的思考核心思路代码实现优缺点分析四、 总结与对比LeetCode 513 - 寻找树的最后一行的最左侧的值&#xff0c;【难度&#xff1a;中等&…

把“评论”菜单从WordPress后台移除的3种方法

在WordPress后台移除“评论”菜单&#xff0c;可以通过以下几种方法实现。以下是详细步骤&#xff1a; 方法1&#xff1a;通过代码移除(推荐) 将以下代码添加到主题的functions.php文件中(或使用CodeSnippets插件)&#xff1a; // 移除后台左侧菜单的“评论” add_action(ad…

大语言模型 LLM 通过 Excel 知识库 增强日志分析,根因分析能力的技术方案(4):只要过一遍LLM的简约版本

文章大纲 只要过一遍LLM的简约版本 1 设计原理(一句话) 2 极简数据流 3 最小依赖实现(本地 SQLite + OpenAI 兼容端点) 3.1 一次性准备:Excel → SQLite 3.2 关键词提取 + 查表(正则 / SQL) 3.3 单次 LLM 调用 4 运行结果示例 5 性能 & Token 对比 6 可扩展点 7 参考…

(转)mybatis和hibernate的 缓存区别?

MyBatis 和 Hibernate 都是流行的 Java 持久化框架&#xff0c;它们都提供了自己的缓存机制来优化数据库操作&#xff0c;减少数据库的访问次数&#xff0c;提高应用程序的性能。尽管两者都支持缓存&#xff0c;但是它们的缓存实现方式和配置有所不同。1. 缓存机制的基本区别My…

【linux内核系列】:万字详解进程间通信:消息队列

&#x1f525; 本文专栏&#xff1a;Linux &#x1f338;作者主页&#xff1a;努力努力再努力wz &#x1f4aa; 今日博客励志语录&#xff1a; 你讨厌的现在&#xff0c;是未来的你拼命想回去修正的战场。 ★★★ 本文前置知识&#xff1a; 匿名管道 命名管道 共享内存 前…

React 19 革命性升级:编译器自动优化,告别手动性能调优时代

概述 React 19 是 React 框架的一个重要里程碑版本&#xff0c;带来了众多突破性的改进和新特性。本文档将详细介绍 React 19 的主要变化&#xff0c;帮助开发者了解并迁移到新版本。 &#x1f680; 主要新特性 React Compiler (编译器) React 19 引入了全新的 React Compi…

UE5的渲染Debug技巧

ShaderPrint UE5相对UE4使用的ComputeShader(GPU Driven)的地方多很多。因为UE5为了方便查看ComputeShader的某些值&#xff0c;开发了“ShaderPrint”&#xff0c;方便直接在Shader 打印信息到屏幕&#xff0c;而不用采用CPUReadback在print的方式。 比如r.nanite.ShowStats…

【2025/08/03】GitHub 今日热门项目

GitHub 今日热门项目 &#x1f680; 每日精选优质开源项目 | 发现优质开源项目&#xff0c;跟上技术发展趋势 &#x1f4cb; 报告概览 &#x1f4ca; 统计项&#x1f4c8; 数值&#x1f4dd; 说明&#x1f4c5; 报告日期2025-08-03 (周日)GitHub Trending 每日快照&#x1f55…

Android系统模块编译调试与Ninja使用指南

模块编译调试方法 (此处举例framework、installd、SystemUI等模块的编译调试&#xff0c;其他类似) 1. Framework模块编译 Android系统代码的framework目录内&#xff0c;一共有3个模块单独编译&#xff1a;framework、services、framework-res.apk。 注意&#xff1a;偶尔会有…

【硬件-笔试面试题】硬件/电子工程师,笔试面试题-51,(知识点:stm32,GPIO基础知识)

目录 1、题目 2、解答 3、相关知识点 一、GPIO 基本结构与特性 1. GPIO 硬件结构 2. 主要特性 二、GPIO 工作模式 1. 输入模式 2. 输出模式 3. 复用功能模式 4. 特殊模式 三、GPIO 配置步骤&#xff08;以 STM32Cube HAL 库为例&#xff09; 1. 初始化 GPIO 时钟 …

小智服务器Java安装编译(xinnan-tech)版

github&#xff1a;https://github.com/xinnan-tech/xiaozhi-esp32-server 一、JDK 1、JDK21下载&#xff1a; https://www.oracle.com/cn/java/technologies/downloads/#jdk21-windows RPM安装&#xff1a; rpm -ivh jdk-21_linux-x64_bin.rpm 2、IDEA设置JDK File → P…