百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

C++ 协程篇一:co_yield和co_return

itomcoil 2025-02-17 12:30 31 浏览

更多互联网精彩资讯、工作效率提升关注【飞鱼在浪屿】(日更新)

这篇博文是两部分系列之一。

  • 第 1 部分:co_yield和co_return
  • 第 2 部分:co_await

介绍

与其他编程语言相比,C++ 加入协程较晚,从C++20开始支持。在协程出现之前,C++ 程序员有两种选择:

  • 同步代码更容易理解但效率较低。
  • 异步代码(例如回调)更高效(让您在等待事情的同时做其他工作)但也更复杂(手动保存和恢复状态)。

协程,“可以暂停执行的函数”,旨在兼顾两全其美:看起来像同步代码但执行起来像异步代码的程序。

一般来说,C++ 语言设计倾向于效率、可定制性和零开销原则, 而不是易用性、安全性之类的东西。

这些既不是“好”也不是“坏”的设计原则,由于 C++ 没有垃圾收集器,也没有运行时系统。这也导致C++ 协程有着陡峭的学习曲线。

这两篇博文并不旨在面面俱到,而是旨在快速浏览三种基本机制(C++20 中新增的协程相关运算符)。这两篇博文都通过一个完整、简单的程序,介绍co_yield,co_return和co_await。


初筛

Eratosthenes 筛法是最早记录的算法之一,已有两千多年的历史,生成了一系列素数:2、3、5、7、11 等。

上个世纪,Doug McIlroy 和 Ken Thompson发明了 Unix 管道作为连接并发进程的一种方式。McIlroy 编写了一页的 C 版本的 Sieve,它使用 Unix 进程和管道。该程序也出现在Tony Hoare颇具影响力的通信顺序过程(CSP) 论文中。最近,Go 也有一个 36 行的 Go 版本的 Sieve 。

该设计可以移植到 C++ 协同程序。CSP 中的“进程”与 Unix 进程不同。我们的程序(与 McIlroy 的程序不同)是单线程和单进程的(在 Unix 进程意义上)。

这里以素数筛选举例,但协程不一定是在 C++ 中实现素数筛选的最佳(最简单、最快等)方式。


输出

构建并运行完整的 C++ 文件,如下所示:

"-fno-exceptions"标志简化了一些 C++程序使用异常的流程。


co_yield

这是一个协同程序(而不是常规函数),因为它的主体中至少有一个显式co_yield或co_return。

虽然常规函数只能返回(比如RType),并且最多只能返回一次,但协程也可以这样做,但在return(CRType)之前可以co_yield零个或多个东西(CYType)。正如常规函数可以永远循环而不返回一样,协程也可以永远循环,可能会执行co_yield某些操作,也可能不会执行co_yield任何操作,而不会co_return。

在这个例子中,source co_yields(生成)整数序列 2、3、4、5 等。因为是协程,所以在它的source末尾有一个隐式语句。co_return;其中RType, CYType和CRType分别是Generator, int 和void。


return和co_return

source返回Generator(即使函数主体从未提及return Generator)。main函数保存调用source结果 ,就像调用常规函数一样。从调用者的角度,以及从“文件中的函数签名.h”的角度来看,它确实只是一个常规函数。与其他编程语言不同,C++ 协程不需要关键字async。

source(40)调用物理上返回(汇编CALL和 RET指令,逻辑上完成后到达 最后一个'}'右半大括号隐式co_return)。这里继续并发运行。对于多线程程序,两者可以并行运行(使用互斥锁、原子或类似)但我们的示例程序是单线程的。concurrency is not parallelism.

从逻辑上讲,source它正在自行运行它的循环for (int x = 2; x < end; x++),偶尔co_yielding 一个东西。物理上,source被调用一次,暂停,返回,然后重复恢复和 co_yielding/suspending 直到以最终的co_return/suspend 结束。

正如我们将在下面进一步看到的,在我们的程序中,恢复是在方法内部显式触发的Generator::next(并且resume只是一个方法调用)。我们的“拉式”生成器协程是“按需”安排的,这在这里工作得很好,因为我们从不等待 I/O。


Promise类型

在常规函数调用中,调用者和被调用者协作(根据调用约定)为堆栈保留一些内存,例如保存函数参数、局部变量、返回地址和返回值。被调用者返回后,栈帧就不再需要了。

对于协程调用,即使在物理返回之后也需要这样的状态(函数参数、局部变量等)。因此,它保存在堆分配的协程框架中。协程框架还包含一些“在协程体内从哪里恢复”的概念,以及一个定制的帮助对象来驱动协程。在 C++ 中,指向协程帧的指针表示为一个std::coroutine_handle.

CustomizedHelper对象被称为“promise”(但它的类型不是std::promise )并且 CustomizedHelper类型通常是RType::promise_type,RType协程的返回类型在哪里。

一些文档谈论“协程状态”而不是“协程框架”,如:promise 对象与“协程框架”(包含参数和局部变量)并存(而不是在其中),两者都在“协程状态”中”。但我更喜欢用“协程框架”来表示整个事情。另请参见frame_ptr下文,作为指向(协程)框架的指针。


Generator::promise_type

在我们的程序中,编译器知道source和filter是协程(因为它们有co_yield表达式)。它们也被声明为返回Generator,因此编译器查找Generator::promise_type并期望它具有某些方法。

例如,我们的协程主体说co_yield x 和CYType (变量x的类型) 是int类型,所以我们的 promise 类型需要有一个yield_value函数带int参数. 它还有一个(隐式)co_return语句(但不是 co_return foo语句),因此它还需要一个return_void不带参数的方法。它还需要get_return_object方法, initial_suspend方法和 final_suspend方法。

这是完整的Generator::promise_type定义:

get_return_object生成Generator对象。我们将 在下面进一步讨论std::coroutine_handle,但它本质上是一个指向协程框架的美化指针。我们会将其传递给构造函数,以便Generator::next 在必要时可以使用协程。

initial_suspend返回一个 awaitable(在篇二中介绍),它控制协程是急切的(也称为“热启动”)还是惰性的(“冷启动”)。协程是直接开始运行还是需要先单独踢一脚?我们的程序返回一个std::suspend_always意思是惰性的,因为这将更好地与“Generator::next总是调用resume以提取下一个值”一起工作,我们将在下面进一步看到。

final_suspend同样控制是否在之后暂停(可能隐含的)co_return。如果它不挂起,协程框架将被自动销毁,从“不要忘记清理”的角度来看这很好,但销毁协程框架也会销毁promise 对象

在我们的程序中,Generator::next需要在co_return之后检查promise 对象(调用 promise 对象的方法仅在协程被挂起时才有效),所以我们挂起(通过final_suspend 返回 a std::suspend_always)。Generator将负责显式销毁协程框架(剧透警报:它将在其析构函数中完成,通过std::coroutine_handle传递给其构造函数)。

yield_value和return_void方法已经提到,yield_value将其参数保存到成员变量( 然后Generator::next将加载)。这就是生成器协程将它产生(产生)的东西传递回消费者的方式。我们的实现一次只缓冲一个值,但其他实现可以做一些不同的事情。至少,如果程序是多线程的,它必须做一些线程安全的事情。


Generator::next

这是Generator::next方法(和Generator构造函数)。它 resume协程,运行到下一次暂停(在显式co_yield或final_suspend隐式之后co_return;后者意味着协程是done)。


资源获取即初始化

要正确清理,我们应该destroy一次std::coroutine_handle。我们将在Generator析构函数中执行此操作(并且该m_cohandle字段是私有的)。当我们将Generator从main传递给filter时,我们必须std::move它,就好像它是一个std::unique_ptr.

g = filter(std::move(g), prime);

调试

在接下来的几个月和几年里它可能会变得更好,但今天调试协程可能有点粗糙,至少在 Debian 稳定版 (Bullseye) 上是这样。断点有效,但局部变量有问题。

例如,我们可以co_yield x在source 协程函数中设置一个断点,但x值似乎没有改变(打印x 总是说 2)并且使断点成为条件意味着x == 5,在实践中,断点不再触发。奇怪的是,info breakpoints还将断点放在_Z6sourcei.actor(_Z6sourcei.frame *)函数中,大概是普通source(int)函数的编译器转换版本。


手动断点

我们可以在源代码中插入手动断点(甚至是条件断点),而不是通过gdb.

在x == 5循环迭代中(但在 之前co_yield),我们的流程(在 CSP 意义上)应该像这样链接main - filter(3) - filter(2) - source:在调试器中重新编译和运行证实了这一点:从下往上,堆栈跟踪显示main,filter两次然后source。

Recall that logically (and in the source code), the filter function takes two arguments (a Generator and an int) but physically (in the stack trace), after the compiler transformed it, filter (or perhaps _Z6filter9Generatori.actor, which c++filt demangles as filter(Generator, int) [clone .actor]) takes only one (what g++ calls the frame_ptr). This pointer value turns out to be the same address as what the std::coroutine_handle::address() method would return. For g++, the frame_ptr address is also a small, constant offset from the promise’s address (what this is inside promise_type methods).


回想一下,从逻辑上(在源代码中),该filter函数有两个参数(Generator和int),但在物理上(在堆栈跟踪中),在编译器转换它之后,filter(或者可能是
_Z6filter9Generatori.actor,c++filt分解为filter(Generator, int) [clone .actor])只接受一个(g++调用的)frame_ptr。这个指针值原来是与该 std::coroutine_handle::address()方法返回的地址相同。对于g++,frame_ptr地址也是相对于promise的地址(promise_type函数)的一个小的常量偏移量。


结论

协程在某种意义上是神奇的,因为它需要编译器支持,并且不是您可以在纯 C++ 中轻松完成的事情(例如,boost 协程依赖于 boost 上下文,并且需要特定于 CPU 体系结构的汇编代码)。但这篇博文有望揭开 C++20 协程co_yield和 co_return运算符的神秘面纱:

  • 如果一个函数的函数体至少包含一个co_yield, co_return或co_await表达式,那么它就是一个协程。
  • 编译器将协程的主体转换为动态分配协程框架的东西。
  • 指向协程框架的指针称为std::coroutine_handle。
  • 该协程框架包含挂起/恢复点、参数和局部变量的副本以及连接调用者和被调用者世界的可自定义帮助器对象(称为承诺对象)。
  • co_yield协程被调用者中的ing(或co_returning)将状态保存在 promise 对象中(通过调用yield_blah或return_blah方法)。调用者(或其他代码)可以稍后加载此状态。
  • co_yielding(或co_returning)是 C++ 语言和标准库的一部分,通常也会暂停协程。
  • 由程序(或其非标准库)明确挂起 resume协程。

最后一个要点掩盖了许多潜在的细节。我们的示例程序相对简单,但总的来说,调度是一个难题。C++20 不提供一刀切的解决方案。它只提供机制,不提供政策。

这部分是因为前面提到的可定制性和“无运行时”设计目标,还因为高性能协程调度实现可能是 OS(操作系统)特定的(你甚至可能没有操作系统

C++20 没有为您提供符合人体工程学的高级协程 API。这不是“撒上一些asyncs 和awaits 就大功告成了”。它为您提供了一个低级 协程 API 构建工具包。需要一些进一步的 C++(但不是汇编)。

Baker 是这样说的:“C++ Coroutines TS [Technical Specification] 在语言中提供的设施可以被认为是协程的低级汇编语言[原文强调]。这些工具很难以安全的方式直接使用,主要供库编写者使用,以构建应用程序开发人员可以安全使用的更高级别的抽象。”

它为您提供了 a 的协程等效项,goto由您(或您使用的库)来构建更好的抽象,例如 if-else 的等效项、while 循环和函数调用。事实上,有些人主张结构化并发,甚至说“Go 语句被认为是有害的”,但更大的讨论超出了本文的范围。


co_await

我要说的最后一件事是co_yield表达式基本上是co_await promise.yield_value(expr)的语法糖。或者,当您可以通过其他方式访问协程的隐式对象,co_await是什么以及它是如何工作的?在第 2 部分中了解更多信息 :co_await。敬请期待。。。

相关推荐

《Queendom》宣布冠军!女团MAMAMOO四人激动落泪

网易娱乐11月1日报道据台湾媒体报道,南韩女团竞争回归的生死斗《Queendom》昨(10/31)晚播出大决赛,并以直播方式进行,6组女团、女歌手皆演唱新歌,并加总前三轮的赛前赛、音源成绩与直播现场投...

正确复制、重写别人的代码,不算抄袭

我最近在一篇文章提到,工程师应该怎样避免使用大量的库、包以及其他依赖关系。我建议的另一种方案是,如果你没有达到重用第三方代码的阈值时,那么你就可以自己编写代码。在本文中,我将讨论一个在重用和从头开始编...

HTML DOM tr 对象_html event对象

tr对象tr对象代表了HTML表格的行。HTML文档中出现一个<tr>标签,就会创建一个tr对象。tr对象集合W3C:W3C标签。集合描述W3Ccells返回...

JS 打造动态表格_js如何动态改变表格内容

后台列表页最常见的需求:点击表头排序+一键全选。本文用原生js代码实现零依赖方案,涵盖DOM查询、排序算法、事件代理三大核心技能。效果速览一、核心思路事件入口:为每个<th>绑...

连肝7个晚上,总结了66条计算机网络的知识点

作者|哪吒来源|程序员小灰(ID:chengxuyuanxiaohui)计算机网络知识是面试常考的内容,在实际工作中经常涉及。最近,我总结了66条计算机网络相关的知识点。1、比较http0....

Vue 中 强制组件重新渲染的正确方法

作者:MichaelThiessen译者:前端小智来源:hackernoon有时候,依赖Vue响应方式来更新数据是不够的,相反,我们需要手动重新渲染组件来更新数据。或者,我们可能只想抛开当前的...

为什么100个前端只有1人能说清?浏览器重排/重绘深度解析

面试现场的"致命拷问""你的项目里做过哪些性能优化?能具体讲讲重排和重绘的区别吗?"作为面试官,我在秋招季连续面试过100多位前端候选人,这句提问几乎成了必考题。但令...

HTML DOM 介绍_dom4j html

HTMLDOM(文档对象模型)是一种基于文档的编程接口,它是HTML和XML文档的编程接口。它可以让开发人员通过JavaScript或其他脚本语言来访问和操作HTML和XML文档...

JavaScript 事件——“事件流和事件处理程序”的注意要点

事件流事件流描述的是从页面中接收事件的顺序。IE的事件流是事件冒泡流,而NetscapeCommunicator的事件流是事件捕获流。事件冒泡即事件开始时由最具体的元素接收,然后逐级向上传播到较为不...

探秘 Web 水印技术_水印制作网页

作者:fransli,腾讯PCG前端开发工程师Web水印技术在信息安全和版权保护等领域有着广泛的应用,对防止信息泄露或知识产品被侵犯有重要意义。水印根据可见性可分为可见水印和不可见水印(盲水印)...

国外顶流网红为流量拍摄性侵女学生?仅被封杀三月,回归仍爆火

曾经的油管之王,顶流网红DavidDobrik复出了。一切似乎都跟他因和成员灌酒性侵女学生被骂到退网之前一样:住在950万美元的豪宅,开着20万美元的阿斯顿马丁,每条视频都有数百万观看...人们仿佛...

JavaScript 内存泄漏排查方法_js内存泄漏及解决方法

一、概述本文主要介绍了如何通过Devtools的Memory内存工具排查JavaScript内存泄漏问题。先介绍了一些相关概念,说明了Memory内存工具的使用方式,然后介绍了堆快照的...

外贸独立站,网站优化的具体内容_外贸独立站,网站优化的具体内容有哪些

Wordpress网站优化,是通过优化代码、数据库、缓存、CSS/JS等内容,提升网站加载速度、交互性和稳定性。网站加载速度,是Google搜索引擎的第一权重,也是SEO优化的前提。1.优化渲染阻塞。...

这8个CSS工具可以提升编程速度_css用什么编译器

下面为大家推荐的这8个CSS工具,有提供函数的,有提供类的,有提取代码的,还有收集CSS的统计数据的……请花费两分钟的时间看完这篇文章,或许你会找到意外的惊喜,并且为你的编程之路打开了一扇新的大门。1...

vue的理解-vue源码 历史 简介 核心特性 和jquery区别 和 react对比

一、从历史说起Web是WorldWideWeb的简称,中文译为万维网我们可以将它规划成如下的几个时代来进行理解石器时代文明时代工业革命时代百花齐放时代石器时代石器时代指的就是我们的静态网页,可以欣...