前言:当本文提到“协程”时,我指的是协程的概念,而不是特定于C++20的功能。当谈论该功能时,我将引用它为“co_await
”或“co_await
协程”。
关于动态分配
Cppreference有时使用比标准更松散的术语。作为一个功能,“co_await
”“需要”动态分配;这种分配是来自堆还是静态内存块或其他的分配者所决定的问题。这些分配可以在任意情况下省略,但由于标准没有明确说明它们,你仍然必须假设任何co_await
协程可能要动态分配内存。
co_await
协程确实有用户为协程状态提供分配的机制。因此,您可以替换任何特定内存池的堆/自由存储器分配。
作为一个功能,“co_await
”被很好地设计用于从任何可等待对象和功能中删除冗余语句。 co_await
机制非常复杂和精细,具有许多类型对象之间的相互作用。但在暂停/恢复点,它始终看起来像co_await <some expression>
。为您的可等待对象和promises添加分配器支持需要一些冗长,但该冗长存在于那些东西被使用的地方之外。
使用
alloca
来实现协程在大多数情况下是高度不适当的,尽管关于这个特性的讨论试图掩盖它,但事实是
co_await
作为一个特性是为异步使用而设计的。这是它的预期目的:暂停函数的执行并将该函数的恢复调度到可能在另一个线程上的位置,然后将任何最终生成的值引导到一些接收代码中,这些代码可能与调用协程的代码相距甚远。
alloca
对于这种特定用例不合适,因为允许/鼓励协程的调用者去做任何事情,以便该值可以由其他线程生成。由
alloca
分配的空间因此将不再存在,这对于驻留其中的协程来说是有问题的。
还要注意,在这种情况下,分配性能通常会被其他考虑所压倒:线程调度、互斥锁和其他东西通常需要正确地安排协程的恢复,更不用说从提供它的任何异步过程中获取该值所需的时间了。因此,在这种情况下需要动态分配并不是一个实质性的考虑因素。
现在,确实存在一些情况下,原地分配是合适的。生成器用例是当您想要暂停函数并返回一个值,然后从函数离开的地方继续执行并可能返回一个新值时使用的情况。在这些情况下,调用协程的函数的堆栈肯定还会存在。
co_await
通过co_yield
支持这些场景,但至少在标准方面,它的效果不是最优的。因为该功能设计用于向上和向外挂起,将其转换为挂起下来的协程会导致产生不需要动态的动态分配。
这就是为什么标准不要求动态分配;如果编译器足够聪明,能够检测到生成器使用模式,则可以删除动态分配并只在本地堆栈上分配空间。但是,这又是编译器可以做到的,而不是必须做到的。
在这种情况下,基于alloca
的分配是合适的。
它如何进入标准
简短的版本是,它进入标准是因为背后的人付出了努力,而替代方案的人没有这样做。
任何协程想法都很复杂,总会有关于实现可行性的问题。例如,“resumeable functions”提案看起来很棒,我很希望看到它成为标准。但是没有人在编译器中实际实现它。因此,没有人可以证明它实际上是可以做的事情。哦,当然,它听起来是可实现的,但那并不意味着它是可实现的。
记住上一次发生的事情,“听起来可行”被用作采用功能的基础。
如果你不知道它能否实现,就不要将其标准化。如果你不知道它是否真正解决了预期的问题,也不要将其标准化。
微软的Gor Nishanov和他的团队投入了多年时间来实现co_await
。他们不断改进自己的实现方式等。其他人在实际生产代码中使用了他们的实现,并且似乎对其功能非常满意。Clang甚至也实现了它。尽管我个人不太喜欢它,但无可否认的是,co_await
是一项成熟的功能。
相比之下,一年前作为与co_await
竞争的替代方案提出的“核心协程”选择没有获得足够的关注部分原因是它们难以实现。这就是为什么采用了co_await
:因为它是一个经过验证、成熟和可靠的工具,人们想要并表现出了改进他们代码的能力。
co_await
并不适合每个人。就我个人而言,我可能不会经常使用它,因为纤程对我的用例更加有效。但是对于它的特定用例——上下文切换非常好。