基于以下内容,C++20 中的协程看起来将是无堆栈的。
https://en.cppreference.com/w/cpp/language/coroutines
我担心的原因有很多:
- 在嵌入式系统上,堆分配通常是不可接受的。
- 在低级代码中,嵌套 co_await 会很有用(我不相信无堆栈协同程序允许这样做)。
使用无堆栈协程,只有顶层例程可以被挂起。由该顶级例程调用的任何例程本身可能不会挂起。这禁止在通用库的例程中提供挂起/恢复操作。
由于需要自定义分配器和内存池,代码更冗长。
如果任务等待操作系统为其分配一些内存(没有内存池),则速度会变慢。
鉴于这些原因,我真的希望我对当前协程的理解非常错误。
问题分为三个部分:
- 为什么 C++ 会选择使用无堆栈协程?
- 关于在无堆栈协程中保存状态的分配。我可以使用 alloca() 来避免通常用于协程创建的任何堆分配。
协程状态通过非数组运算符 new 在堆上分配。 https://en.cppreference.com/w/cpp/language/coroutines
- 我对 c++ 协程的假设是否错误,为什么?
编辑:
我现在正在为协程进行 cppcon 会谈,如果我找到我自己问题的任何答案,我会发布它(到目前为止还没有)。
CppCon 2014:Gor Nishanov “等待 2.0:无堆栈可恢复函数”
https://www.youtube.com/watch?v=KUhSjfSbINE
CppCon 2016:James McNellis “C++ 协程简介”
https://www.youtube.com/watch?v=ZTqHjjm86Bw
原文由 David Ledger 发布,翻译遵循 CC BY-SA 4.0 许可协议
转发:当这篇文章只说“协程”时,我指的是协程的 _概念_,而不是特定的 C++20 特性。在谈到这个特性时,我将它称为“
co_await
”或“co_await coroutines”。关于动态分配
Cppreference 有时使用比标准更宽松的术语。
co_await
作为特性“需要”动态分配;这种分配是来自堆还是来自静态内存块,或者分配提供者的任何事情。这种分配可以在任意情况下省略,但由于标准没有明确说明,您仍然必须假设任何 co_await 协程都可以动态分配内存。co_await 协程确实有机制让用户为协程的状态提供分配。因此,您可以将堆/空闲存储分配替换为您喜欢的任何特定内存池。
co_await
作为一项功能经过精心设计,可以从任何co_await
对象和功能的使用点 消除 冗长。co_await
机器非常复杂和错综复杂,在多种类型的对象之间有大量的交互。但在暂停/恢复点,它 总是 看起来像co_await <some expression>
。为您的可等待对象和承诺添加分配器支持需要一些冗长,但这种冗长存在于使用这些东西的地方之外。将
alloca
用于协程将……非常不适合co_await
的 大多数 用途。虽然围绕此功能的讨论试图隐藏它,但事实是co_await
作为一个功能是为异步使用而设计的。这就是它的预期目的:停止函数的执行并安排该函数在可能的另一个线程上恢复,然后将任何最终生成的值引导到一些接收代码,这些代码可能与调用协程的代码有些距离。alloca
不适合该特定用例,因为允许/鼓励协程的调用者去做任何事情,以便可以由其他线程生成该值。由alloca
分配的空间因此将不复存在,这对其中的协程来说是不利的。另请注意,在这种情况下,分配性能通常会因其他考虑而相形见绌:通常需要线程调度、互斥锁和其他东西来正确安排协程的恢复,更不用说从任何异步获取值所需的时间了过程正在提供它。因此,在这种情况下,需要动态分配这一事实并不是真正需要考虑的问题。
现在,在 某些 情况下,就地分配是合适的。生成器用例适用于您想要暂停一个函数并返回一个值,然后从函数停止的地方开始并可能返回一个新值。在这些情况下,调用协程的函数的堆栈肯定仍然存在。
co_await
支持这样的场景(虽然co_yield
),但它以一种不太理想的方式这样做,至少就标准而言。因为该功能是为上下挂起而设计的,所以将其变成一个挂起协程具有不需要动态的动态分配的效果。这就是标准不需要动态分配的原因;如果编译器足够聪明,可以检测到生成器的使用模式,那么它可以删除动态分配并只在本地堆栈上分配空间。但同样,这是编译器 可以 做的,而不是必须做的。
在这种情况下,基于
alloca
的分配将是合适的。它是如何进入标准的
简短的版本是它进入标准是因为它背后的人投入了工作,而替代方案背后的人没有。
任何协程的想法都是复杂的,并且总会有关于它们的可实施性的问题。例如,“ 可恢复功能”提案看起来很棒,我很想在标准中看到它。但是没有人真正在编译器中 实现 它。所以没有人能证明这实际上是你可以做的事情。哦,当然,这 听起来 可以实现,但这并不意味着它 是可以 实现的。
请记住 上次使用“听起来可实现”作为采用功能的基础时发生的事情。
如果你不知道它可以被实施,你就不想标准化一些东西。如果你不知道它是否真的解决了预期的问题,你也不想标准化。
Gor Nishanov 和他在微软的团队投入工作以实现
co_await
。他们这样做了很多 _年_,改进了他们的实施等等。其他人在实际的生产代码中使用了他们的实现,并且似乎对它的功能非常满意。 Clang 甚至实现了它。尽管我个人不喜欢它,但不可否认,co_await
是一个 成熟 的功能。相比之下,一年前作为与
co_await
竞争的想法提出的“核心协程”替代方案未能获得关注 ,部分原因是它们难以实施。这就是为什么co_await
被采用的原因:因为它是一种经过验证的、成熟的、可靠的工具,人们想要并且已经证明了改进代码的能力。co_await
并不适合所有人。就我个人而言,我可能不会经常使用它,因为纤维对我的用例来说效果更好。但它非常适合其特定用例:上下悬挂。