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

时间:2020-07-18 11:05:03

标签: c++ asynchronous coroutine c++20 c++-coroutine

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

这些天我正在学习C ++ 20协程。我从Coroutines Introduction了解了有关堆栈式和协程式协程的信息。我也特别想了解更多信息。

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

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

  2. 当其自身挂起时,由于无堆栈协程只能在顶级函数处挂起,因此其堆栈是可预测的,有用的数据将存储在特定区域中。

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

协程TS指定在为协程帧分配存储空间时调用非数组operator new。但是,我认为这是不必要的,因此是我的问题。

一些解释/考虑:

  1. 将协程的位置放在何处?在最初存储指针的句柄中。

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

  3. 来自cppreference

    如果可以,可以优化对new运算符的调用(即使使用了自定义分配器)

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

    • 在呼叫站点知道协程框架的大小

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

    另一方面,如果调用方不知道大小,如何构成调用operator new的参数?实际上,我什至无法想象呼叫者在什么情况下不知道大小。

  4. 根据{{​​3}},
  5. 铁锈似乎有不同的实现方式。

3 个答案:

答案 0 :(得分:4)

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

那是您误会的根源。

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

这样,就不能假定将其放在“调用方的堆栈上”,因为调用方和安排协程恢复的进程不一定在同一线程中。协程必须能够使调用程序的寿命更长,因此协程的堆栈不能位于调用方的堆栈上(通常。在某些if (array.length ===0 ) { return array; } else { for (i = 0; i < array.length; i++) { if (array[i].length < tiny.length) { tiny = array[i]; } } return tiny; } } 风格的情况下,可以)。

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

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

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

将其称为“句柄”不会改变它的含义。

但是我的目的是忽略对co_yield的调用,无论它如何实现。

嗯...你不能。如果从不调用operator new是编写所写软件的重要组成部分,则不能使用new风格的协程延续。没有可以使用的规则集来保证协程中co_await的省略。如果您使用的是特定的编译器,则可以进行一些测试以查看其隐藏的内容和未隐藏的内容,

您引用的规则仅仅是使可能取消通话的情况。

另一方面,如果调用方不知道大小,如何构成调用new的参数?

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

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

答案 1 :(得分:2)

考虑这个假设情况:

void foo(int);

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

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

用于功能栈的内存必须以在暂停和后续恢复之间保存的方式分配。但是,如果某些对象引用了该内存中的对象(或者至少没有增加复杂性),则无法移动或复制该内存。这就是为什么有时编译器需要在堆上分配此内存的原因。

答案 2 :(得分:1)

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

在一个堆栈式协程中,协程的局部变量像在执行期间和挂起时一样,存储在它拥有的堆栈中。

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

理论中,可以将无栈协程存储在某人的栈中。但是,无法在C ++代码中保证会发生这种情况。

在创建协程时,省略new运算符就可以做到这一点。如果您的协程对象存储在某人的堆栈上,并且因为协程对象本身具有足够的状态空间而删除了新协程对象,则可能完全生活在其他人的堆栈上的无堆栈协程。

在当前的C ++协程实现中,没有任何方法可以保证这一点。试图在编译器开发人员遇到阻力的情况下实现这一目标,因为协程的确进行的最小捕获发生的时间“晚于”他们需要知道协程中有多大的时间。


这导致实践上的差异。堆栈式协程的行为更像是线程。您可以调用常规函数,这些常规函数可以在其体内与协程操作(例如暂停)相互作用。

无栈协程不能调用函数,然后与协程机器进行交互。仅在无堆栈协程自身内部才允许与协程机器进行交互。

堆栈式协程具有线程的所有机制,而没有在OS上进行调度。无栈协程是一种增强的功能对象,其中带有goto标签,可以使它通过其主体部分恢复。


有一些无栈协程的理论实现,这些实现没有“可以调用新的”功能。 C ++标准不需要这种无堆栈协程。

有人提出了他们。他们的建议输给了当前的建议,部分原因是与其他建议相比,当前的建议更精致,更易于发货。替代提案的某些语法最终以成功的提案结束。

我相信,有一个令人信服的论点,即“限制器”固定大小的无新协程实现在当前提案中并未排除,可以在以后添加,这有助于终止其他提案。