Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >C++ 协程篇一:co_yield和co_return

C++ 协程篇一:co_yield和co_return

原创
作者头像
mariolu
发布于 2023-02-25 06:58:34
发布于 2023-02-25 06:58:34
2.8K00
代码可运行
举报
运行总次数:0
代码可运行

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

  • 第 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>.

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.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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<Generator::promise_type>::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<Generator::promise_type>::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。敬请期待。。。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
c++20的协程学习记录(三): co_yield和co_return操作符
https://cloud.tencent.com/developer/article/2375995
mariolu
2024/01/03
5850
C++20协程初探!
导语 | 本文推选自腾讯云开发者社区-【技思广益 · 腾讯技术人原创集】专栏。该专栏是腾讯云开发者社区为腾讯技术人与广泛开发者打造的分享交流窗口。栏目邀约腾讯技术人分享原创的技术积淀,与广泛开发者互启迪共成长。本文作者是腾讯后台开发工程师杨良聪。 协程(coroutine)是在执行过程中可以被挂起,在后续可以被恢复执行的函数。在C++20中,当一个函数内部出现了co_await、co_yield、co_return中的任何一个时,这个函数就是一个协程。 C++20协程的一个简单的示例代码:
腾讯云开发者
2022/09/29
1.2K0
C++20协程初探!
C++20 协程:异步编程的演进
C++20 引入的协程(Coroutines)为异步编程和并发任务提供了一种新的范式。与传统线程模型相比,协程以更低的切换开销和更直观的代码结构优化了资源密集型任务的处理。本文将探讨协程的机制、核心组件及其在现代 C++ 中的应用。
码事漫谈
2025/03/05
1340
C++20 协程:异步编程的演进
协程及c++ 20原生协程研究报告 下
上一章节介绍了协程的现状,并以libco为例介绍了主流有栈协程的实现原理。这一篇,我们开始进入C++20原生协程的研究。
JohnYao
2022/06/29
1.1K1
协程及c++ 20原生协程研究报告 下
C++20新特性解析:深入探讨协程库的实现原理与应用
C++20引入了对协程的支持,这是一项重要的编程语言特性,可以简化异步编程的实现而且提高代码的可读性和可维护性。协程可以在执行过程中暂停和恢复,能够更直观地表达异步操作的流程,让编程更加简洁和高效。
Lion 莱恩呀
2025/04/23
1610
C++20新特性解析:深入探讨协程库的实现原理与应用
C++协程从入门到精通
协程(coroutine)是一种特殊的函数,它可以被暂停(suspend)、恢复执行(resume),并且一个协程可以被多次调用。C++中的协程属于stackless协程,即协程被suspend时不需要堆栈。C++20开始引入协程,围绕协程实现的相应组件较多,如co_wait、co_return、co_yield,promise,handle等组件,灵活性高,组件之间的关系也略复杂,这使得C++协程学习起来有一定难度。
码事漫谈
2025/05/05
1400
C++协程从入门到精通
C++一分钟之-认识协程(coroutine)
协程(Coroutine)是C++20引入的一项重要特性,它为程序设计提供了更高层次的控制流抽象,允许非阻塞式的异步编程模型,而无需复杂的回调函数或者状态机。本文旨在深入浅出地介绍C++协程的基本概念、使用场景、常见问题、易错点及避免策略,并通过实例代码加深理解。
Jimaks
2024/06/30
7300
C++一分钟之-认识协程(coroutine)
协程(Coroutine)是C++20引入的一项重要特性,它为程序设计提供了更高层次的控制流抽象,允许非阻塞式的异步编程模型,而无需复杂的回调函数或者状态机。本文旨在深入浅出地介绍C++协程的基本概念、使用场景、常见问题、易错点及避免策略,并通过实例代码加深理解。
Jimaks
2024/07/01
6600
C++一分钟之-认识协程(coroutine)
C++20 Coroutine
最近的新闻里 C++20 已经确认的内容里已经有了协程组件,之前都是粗略看过这个协程草案。最近抽时间更加系统性的看了下接入和实现细节。
owent
2020/01/02
3.1K0
c++20的协程学习记录(二): 初探ReturnObject和Promise
c++20的协程学习记录(一): 初探co_await和std::coroutine_handle<>
mariolu
2024/01/02
3620
从无栈协程到 C++异步框架
作者:fangshen,腾讯 IEG 游戏客户端开发工程师 导语 本文我们将尝试对整个 C++的协程做深入浅出的剥析, 方便大家的理解. 再结合上层的封装, 最终给出一个 C++异步框架实际业务使用的一种形态, 方便大家更好的在实际项目中应用无栈协程。 1. 浅谈协程 在开始展开协程前, 我们先来看一下一些非 C++语言中的协程实现. 1.1 其他语言中的协程实现 很多语言里面, 协程是作为 "一类公民" 直接加入到语言特性中的, 比如: 1.1.1 Dart1.9 示例代码 Future<int> get
腾讯技术工程官方号
2022/10/13
2.6K0
从无栈协程到 C++异步框架
libcopp对C++20协程的接入和接口设计
最近开的坑有点多。有点忙不过来了所以好久没写Blog了。这个C++20的协程接入一直在改造计划中,但是一直没抽出时间来正式实施。 在之前,我写过一个初版的C++20协程接入 《libcopp接入C++20 Coroutine和一些过渡期的设计》 。当时主要是考虑到 Rust也有和C++类似的历史包袱问题,所以参考了一些Rust协程改造过程中的设计。 但是后来尝试在项目中使用的时候发现还是有一些问题。首先C++20的协程并不是零开销抽象,所以强行用Rust的模式反而带来了一定开销和理解上的难度。其次原先的设计中 generator 是按类型去实现外部接入的。但是实际接入SDK的过程中我们有相当一部分类型相同但是接入流程不同的情况,再加上现在各大编译器也都已经让C++20协程的特性脱离 experimental 阶段了,有一些细节有所变化。所以干脆根据我们实际的使用场景,重新设计了下组织结构。
owent
2023/03/06
7230
万字好文:从无栈协程到C++异步框架!
点个关注👆跟腾讯工程师学技术 导语 | 本文我们将尝试对整个 C++的协程做深入浅出的剖析,方便大家的理解。再结合上层的封装,最终给出一个 C++异步框架实际业务使用的一种形态,方便大家更好的在实际项目中应用无栈协程。 浅谈协程 在开始展开协程前,我们先来看一下一些非 C++语言中的协程实现。 (一)其他语言中的协程实现 很多语言里面,协程是作为 "一类公民" 直接加入到语言特性中的, 比如:  Dart1.9示例代码 Future<int> getPage(t) async {
腾讯云开发者
2022/11/09
1.2K0
万字好文:从无栈协程到C++异步框架!
如何在C++20中实现Coroutine及相关任务调度器?(实例教学)
导语 | 本篇文章循序渐进地介绍C++20协程的方方面面,先从语言机制说起,再来介绍如何基于C++20的设施实现一个对比C++17来说更简单易用,约束性更好的一个任务调度器,最后结合一个简单的实例来讲述如何在开发中使用这些基础设施。 Vue框架通过数据双向绑定和虚拟DOM技术,帮我们处理了前端开发中最脏最累的DOM操作部分,我们不再需要去考虑如何操作DOM以及如何最高效地操作DOM,但是我们仍然需要去关注Vue在跨平台项目性能方面的优化,使项目具有更高效的性能、更好的用户体验。 一、C++20 Cor
腾讯云开发者
2021/09/26
3.2K0
打通游戏服务端框架的C++20协程改造的最后一环
我们终于在年初的时候最后完成了整体服务器框架对C++20协程的支持和接入。虽然之前陆陆续续抽时间改造一些组件,让它支持C++20协程,期间也记录了一些早期的设计思路和踩的坑(包括 《libcopp接入C++20 Coroutine和一些过渡期的设计》和《libcopp对C++20协程的接入和接口设计》),其中不乏一些C++20协程使用上可能打破我们常规思路细节和编译器的BUG。而且这些都是各个组件的改造,并没有最后整合到一起。
owent
2023/04/12
7050
打通游戏服务端框架的C++20协程改造的最后一环
C++20 Coroutine 性能测试 (附带和libcopp/libco/libgo/goroutine/linux ucontext对比)
之前写了 《协程框架(libcopp)v2优化、自适应栈池和同类库的Benchmark对比》 和 《C++20 Coroutine》 ,但是一直没写 C++20 Coroutine 的测试报告。
owent
2020/01/02
4.1K0
C++ 动态新闻推送 第6期
从reddit/hackernews/lobsters/meetingcpp摘抄一些c++动态。
王很水
2021/08/31
5080
c++20的协程学习记录(一): 初探co_await和std::coroutine_handle<>
在讲协程之前,先回顾C11之前我们怎么处理多任务,怎么同步不同任务之间的处理顺序。想象一个你在用文本编辑器GUI,你对GUI的每个button进行操作,背后都有一段函数代码处理你的button事件。这就是事件驱动。事件驱动代码的一个典型示例是注册一个回调,每次套接字有数据要读取时都会调用该回调。
mariolu
2024/01/01
1.6K0
使用 c++20 协程与 io_uring 实现高性能 web 服务器 part1:一个最简单的 echo server
如果您不熟悉 io_uring 和 c++20 协程,可以参考这个仓库里的其他一些文章和示例代码:
云微
2023/02/24
9710
使用 c++20 协程与 io_uring 实现高性能 web 服务器 part1:一个最简单的 echo server
C++ 动态新闻推送 第30期
从reddit/hackernews/lobsters/meetingcpp摘抄一些c++动态。
王很水
2021/09/23
5030
C++ 动态新闻推送 第30期
推荐阅读
相关推荐
c++20的协程学习记录(三): co_yield和co_return操作符
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验