是否可以在 C++ 中制作零分配协程运行时?

Vad*_*P22 21 c++ asynchronous language-design c++20 c++-coroutine

在 Rust 中,异步函数不需要在堆上分配。函数async返回编译器生成的结构,您可以在该结构上调用编译器生成的poll方法。异步的设计看起来逻辑清晰。

C++中协程的设计很奇怪。它迫使您在堆上进行分配,因为您没有其他方法可以使用co_return.

(可以创建一个自定义分配器来在堆栈上的缓冲区中进行分配,但这会使代码不必要地复杂化。)

为什么在 C++ 的设计中决定协程返回的对象必须有一个promise_type

为什么await_ready,,,await_suspendawait_resume不够?

(这看起来很奇怪,这就是强制你进行分配的原因;你不能直接构造一个SomeTask<T>对象(使用三个await_*方法)并返回它。)

我们如何在没有自定义分配器的情况下制作零分配协程?

Nic*_*las 20

本质上,它迫使您在堆上进行分配,因为您没有其他方法可以使用co_return

这不是分配的原因。

协程需要存储来完成其工作。不仅返回值的编组,而且还用于跟踪整个事情。协程的堆栈,即必须暂停和恢复的东西,是其中的一部分。

协程的全部意义在于它可以暂停执行并让某人恢复执行。这个人是谁并不能静态地获知。这就是为什么需要动态分配的原因;协程的生命周期不受其初始调用者范围的限制。

如果函数 X 调用协程 A,并且协程 A 挂起自身以等待某个异步请求,则函数 X 可能会将协程返回给等待它的其他人。这意味着函数 X 的调用堆栈消失了。如果协程 A 存在于函数 X 的堆栈上,那么它的存储空间现在就消失了。

那很糟。

C++ 将协程 A 存储在动态分配的内存中,以使其能够在调用中存活下来。这是协程的假定默认情况。

据推测,您所描述的情况是函数 X 本身是等待 A 的协程,或者在 A 完成之前不会离开自己的作用域。这将允许 A 的协程状态毫无问题地存在于 X 的堆栈上。如果检测到这些条件,C++ 编译器可以对其进行优化;他们可以省略动态分配。

但除此之外,动态分配是必要的

我不知道鲁斯特。所以我不知道 Rust 的async功能是做什么的。也许它的异步函数无法持续超出其调用范围的边界。也许 Rust 能够更好地检测异步函数何时需要能够超出其调用者的范围。也许您必须明确告诉 Rust 当您希望协程逃脱其调用者的作用域时。我不知道。

但 C++ 不是这样的。C++ 协程被假定离开其调用函数的范围。

  • 供参考:Rust 闭包(lambda 和协程)被实现为无名结构,将捕获的值保存为字段 - 通过引用(在这种情况下,Rust 的借用检查器将阻止外部作用域在捕获的引用处于活动状态时结束)或通过移动。Rust 的协程是无堆栈的,而是捕获它们需要轮询的任何其他协程。这意味着协程具有已知的大小并且可以进行堆栈分配。 (3认同)
  • 我自己是一个 Rust 新手,但是... Rust 没有单独的编译,因此它完全避免了这个问题,并且调用者知道协程状态的大小和结构。Rust future 总是很懒惰的(我们有无限的定制),并且在开始执行之前,固定性不是问题......所以您可以安全地向上移动 future,存储它或移动它,直到您需要开始执行。所以他们有办法“固定”通常可移动的东西。 (3认同)