为什么无堆栈协程需要动态分配?

jer*_*uyi 5 c++ asynchronous coroutine c++20 c++-coroutine

这个问题不是关于 C++20 中的协程,而是关于一般的协程。

这几天我正在学习C++20协程。我从协程简介中了解了堆栈协程和无堆栈协程。我还索取了更多信息。

以下是我对无堆栈协程的理解:

  1. 无堆栈协程在运行时在调用者的堆栈上确实有堆栈。

  2. 当它自行挂起时,由于无堆栈协程只能挂在顶层函数,因此它的堆栈是可预测的,并且有用的数据存储在某个区域中。

  3. 当它不运行时,它没有堆栈。它与一个句柄绑定,客户端可以通过该句柄恢复协程。

operator new协程 TS 指定为协程帧分配存储时调用非数组。然而,我认为这是没有必要的,因此我的问题。

一些解释/考虑:

  1. 协程的状态应该放在哪里?在句柄中,原来存储的是指针。

  2. 动态分配并不意味着存储在堆上。但我的目的是消除对 的调用operator new,无论它是如何实现的。

  3. 来自cppreference

    如果满足以下条件,则可以优化对operator new 的调用(即使使用自定义分配器):

    • 协程状态的生命周期严格嵌套在调用者的生命周期内,并且

    • 协程框架的大小在调用站点是已知的

    对于第一个要求,如果协程比调用者寿命更长,则将状态直接存储在句柄中仍然可以。

    另一方面,如果调用者不知道大小,它如何编写调用的参数operator new?事实上,我什至无法想象在什么情况下调用者不知道尺寸。

  4. 根据这个问题,Rust 似乎有不同的实现。

Nic*_*las 7

无堆栈协程在运行时在调用者的堆栈上确实有堆栈。

这就是你误解的根源。

基于连续的协程(即“无堆栈协程”)是一种协程机制,旨在为某些其他代码提供协程,这些代码将在某些异步进程完成后恢复其执行。此恢复可能发生在其他线程中。

因此,不能假定堆栈位于“调用者的堆栈上”,因为调用者和调度协程恢复的进程不一定位于同一线程中。协程需要能够比调用者更长寿,因此协程的堆栈不能位于调用者的堆栈上(一般情况下。在某些co_yield-style 情况下,可以)。

协程句柄代表协程的堆栈。只要该句柄存在,协程的堆栈也存在。

当它不运行时,它没有堆栈。它与一个句柄绑定,客户端可以通过该句柄恢复协程。

这个“句柄”如何存储协程的所有局部变量?显然它们被保留了(如果没有的话,这将是一个糟糕的协程机制),所以它们必须存储在某个地方。函数局部变量所在位置的名称称为“堆栈”。

称其为“句柄”并不会改变它的本质。

但我的目的是消除对 的调用operator new,无论它是如何实现的。

嗯...你不能。如果从不调用new是编写您正在编写的任何软件的重要组成部分,那么您就不能使用co_await-style 协程延续。没有可以使用的规则集来保证new协程中的省略。如果您使用特定的编译器,您可以进行一些测试来查看它省略了什么和没有什么,但仅此而已。

您引用的规则仅仅是可以忽略呼叫的情况。

另一方面,如果调用者不知道大小,它如何编写调用的参数operator new

请记住:co_awaitC++ 中的协程实际上是函数的实现细节。调用者不知道它调用的任何函数是否是协程。所有协程从外部看起来都像常规函数。

创建协程堆栈的代码发生在函数调用内,而不是在函数调用之外。


Yak*_*ont 5

有堆栈和无堆栈协程之间的根本区别在于,协程是否像线程一样拥有完整的、理论上无界的堆栈(但实际上是有界的)。

在堆栈协程中,协程的局部变量在执行期间和挂起时都存储在它拥有的堆栈中,就像其他任何东西一样。

在无堆栈协程中,无论协程运行或不运行,协程的局部变量都可以位于堆栈中。它们存储在无堆栈协程拥有的固定大小的缓冲区中。

理论上,无堆栈协可以存储在其他人的堆栈上。然而,在 C++ 代码中没有办法保证这种情况发生。

在创建协程时省略 new 运算符就是这样做的。如果你的协程对象存储在某人的堆栈上,并且 new 被省略,因为协程对象本身有足够的空间容纳其状态,那么完全生活在其他人的堆栈上的无堆栈协程是可能的。

在当前的 C++ 协程实现中,无法保证这一点。尝试实现这一点遇到了编译器开发人员的阻力,因为协程执行的精确最小捕获发生的时间“晚于”他们需要知道编译器中协程有多大的时间。


这就导致了实践中的差异。堆栈协程的行为更像是线程。您可以调用普通函数,这些普通函数可以在其内部与挂起等协程操作进行交互。

无堆栈协程无法调用函数然后与协程机制交互。仅允许在无堆栈协程本身内与协程机制进行交互。

堆栈协程具有线程的所有机制,无需在操作系统上进行调度。无堆栈协程是一个增强函数对象,其中具有 goto 标签,可以使其在其主体中部分恢复。


无堆栈协程的理论上的实现不具有“可以调用新”功能。C++ 标准不需要这种类型的无堆栈协程。

有些人提出了它们。他们的提案输给了当前的提案,部分原因是当前的提案比其他提案更加完善且更接近交付。替代提案的一些语法最终出现在成功的提案中。

我相信有一个令人信服的论点,即当前提案并未排除“更严格”的固定大小无新协程实现,并且可以在之后添加,这有助于扼杀替代提案。


Oli*_*liv 3

考虑这个假设的情况:

void foo(int);

task coroutine() {
    int a[100] {};
    int * p = a;
    while (true) {
       co_await awaitable{};
       foo (*p);
       }
    }
Run Code Online (Sandbox Code Playgroud)

p指向 的第一个元素a,如果在两次恢复之间,a的内存位置发生变化,p则不会保存正确的地址。

函数堆栈的内存必须以这样的方式分配,即在挂起和随后的恢复之间保留它。但是,如果某些对象引用此内存中的对象(或者至少在不增加复杂性的情况下不能移动或复制),则无法移动或复制此内存。这就是为什么编译器有时需要在堆上分配内存的原因。