【问题标题】:How do stackless coroutines differ from stackful coroutines?无堆栈协程与有堆栈协程有何不同?
【发布时间】:2015-05-12 16:37:28
【问题描述】:

背景:

我问这个是因为我目前有一个包含许多(成百上千)线程的应用程序。大多数这些线程大部分时间都处于空闲状态,等待将工作项放入队列中。当一个工作项可用时,它会通过调用一些任意复杂的现有代码来处理。在某些操作系统配置中,应用程序会遇到控制最大用户进程数的内核参数,因此我想尝试减少工作线程数量的方法。

我提出的解决方案:

这似乎是一种基于协程的方法,我用协程替换每个工作线程,这将有助于实现这一目标。然后,我可以拥有一个由实际(内核)工作线程池支持的工作队列。当一个项目被放置在特定协程的队列中进行处理时,一个条目将被放置到线程池的队列中。然后它将恢复相应的协程,处理其排队的数据,然后再次挂起,释放工作线程来做其他工作。

实现细节:

在考虑如何做到这一点时,我很难理解无堆栈协程和有堆栈协程之间的功能差异。我有一些使用 Boost.Coroutine 库的堆栈协程的经验。我发现从概念层面理解起来相对容易:对于每个协程,它维护一份 CPU 上下文和堆栈的副本,​​当您切换到协程时,它会切换到保存的上下文(就像内核模式调度程序一样)。

我不太清楚的是无堆栈协程与此有何不同。在我的应用程序中,与上述工作项排队相关的开销非常重要。我见过的大多数实现,比如the new CO2 library,都表明无堆栈协程提供了开销更低的上下文切换。

因此,我想更清楚地了解无堆栈和堆栈式协程之间的功能差异。具体来说,我想到了这些问题:

  • References like this one 建议区别在于您可以在堆栈和无堆栈协程中产生/恢复的位置。是这样吗?有没有一个简单的例子说明我可以在堆栈式协程中但不能在无堆栈式协程中做某事?

  • 对使用自动存储变量(即“堆栈上”的变量)是否有任何限制?

  • 我可以从无堆栈协程调用哪些函数有任何限制吗?

  • 如果没有为无堆栈协程保存堆栈上下文,那么协程运行时自动存储变量会去哪里?

【问题讨论】:

  • '大部分线程大部分时间都处于空闲状态,等待将工作项放入队列' - 如果是这种情况,为什么会有这么多线程?
  • @MartinJames:出于遗留原因。我并不是说它是一个好的设计,因此我希望改进它。批量重构整个应用程序不是近期的选择,因此我正在寻找相对简单的改造开始。可能使事情进一步复杂化,对队列的阻塞调用通常在调用堆栈的几个层次上进行(即不在工作线程的顶级函数处)。我认为这将排除在此特定上下文中使用无堆栈线程。
  • 另见boost::asio

标签: c++ concurrency coroutine boost-coroutine


【解决方案1】:

首先,感谢您查看CO2 :)

Boost.Coroutine doc 很好地描述了堆栈式协程的优势:

堆叠性

与无堆栈协程相比堆栈协程 可以从嵌套的堆栈帧中暂停。执行恢复于 代码中与之前暂停的完全相同的点。和 一个无堆栈的协程,只有顶层的例程可以被挂起。 由该顶级例程调用的任何例程本身可能不会挂起。 这禁止在例程中提供暂停/恢复操作 一个通用库。

一流的延续

一流的延续可以传递为 一个参数,由函数返回并存储在数据结构中 以后使用。在某些实现中(例如 C# yield) continuation 不能直接访问或直接操作。

没有堆栈性和一流的语义,一些有用的执行 不能支持控制流(例如合作 多任务或检查点)。

这对你意味着什么?例如,假设您有一个接收访问者的函数:

template<class Visitor>
void f(Visitor& v);

你想把它转成迭代器,用stackful coroutine,你可以:

asymmetric_coroutine<T>::pull_type pull_from([](asymmetric_coroutine<T>::push_type& yield)
{
    f(yield);
});

但是对于无堆栈协程,没有办法这样做:

generator<T> pull_from()
{
    // yield can only be used here, cannot pass to f
    f(???);
}

一般来说,有栈协程比无栈协程更强大。 那么为什么我们需要无堆栈协程呢?简短的回答:效率。

堆栈式协程通常需要分配一定数量的内存来容纳其运行时堆栈(必须足够大),并且与无堆栈的相比,上下文切换更昂贵,例如在我的机器上,Boost.Coroutine 需要 40 个周期,而 CO2 平均只需要 7 个周期,因为无堆栈协程唯一需要恢复的就是程序计数器。

也就是说,在语言支持的情况下,只要协程中没有递归,stackful coroutine 可能也可以利用编译器计算的栈的 max-size,因此也可以提高内存使用率。

说到无堆栈协程,请记住,这并不意味着根本没有运行时堆栈,只是意味着它使用与主机端相同的运行时堆栈,因此您也可以调用递归函数,只是所有的递归都将发生在主机的运行时堆栈上。相比之下,堆栈式协程,当您调用递归函数时,递归将发生在协程自己的堆栈上。

回答问题:

  • 自动存储变量的使用是否有限制 (即“在堆栈上”的变量)?

没有。这是CO2的仿真限制。在语言支持下,自动存储变量对协程可见将被放置在协程的内部存储中。请注意我对“协程可见”的强调,如果协程调用内部使用自动存储变量的函数,那么这些变量将被放置在运行时堆栈中。更具体地说,无堆栈协程只需要保留恢复后可以使用的变量/临时变量。

要明确一点,你也可以在 CO2 的协程体中使用自动存储变量:

auto f() CO2_RET(co2::task<>, ())
{
    int a = 1; // not ok
    CO2_AWAIT(co2::suspend_always{});
    {
        int b = 2; // ok
        doSomething(b);
    }
    CO2_AWAIT(co2::suspend_always{});
    int c = 3; // ok
    doSomething(c);
} CO2_END

只要定义不在任何await之前。

  • 我可以从 无堆栈协程?

没有。

  • 如果没有为无堆栈协程保存堆栈上下文, 当协程运行时,自动存储变量在哪里 跑步吗?

如上所述,无堆栈协程不关心被调用函数中使用的自动存储变量,它们只会放在正常的运行时堆栈上。

如果您有任何疑问,只需查看 CO2 的源代码,它可能会帮助您了解引擎盖下的机制;)

【讨论】:

    【解决方案2】:

    您想要的是用户级线程/纤程 - 通常您希望将代码(在纤程中运行)暂停在深层嵌套调用堆栈中(例如解析来自 TCP 连接的消息)。在这种情况下,您不能使用无堆栈上下文切换(应用程序堆栈在无堆栈协程之间共享 -> 调用的子程序的堆栈帧将被覆盖)。

    您可以使用 boost.fiber 之类的东西,它基于 boost.context 实现用户级线程/纤维。

    【讨论】:

    • 我使用纤程或协程实现这一点的主要挑战是调度问题。我想实现一个 M:N 线程模型,其中 N 个纤程/协程由 M 个内核线程提供服务,但我希望这些 M 个线程能够根据需要为任何 N 个纤程/协程提供服务。是否可以从不同于之前暂停的内核线程恢复boost::fiber?这样做会影响性能吗? boost::asymmetric_coroutine 呢?
    • 将光纤从一个线程迁移到另一个线程目前正在开发中,但我不建议这样做。只有在另一个 CPU 上执行光纤时,移动一根光纤才有意义。将光纤从一个 CPU 移动到另一个 CPU 会导致缓存未命中。 boost.fiber 为 Fiber 提供了诸如 mutext/条件变量等同步类。
    • 感谢您的信息。我确实觉得这将是一个有用的功能。就像我说的,在我的应用程序中,我想做 M:N 线程。我会有 N 根纤维,每根纤维代表一个处理管道。这些管道都是从另一个线程异步馈送数据的。因此,我希望能够使用 M 个(远小于 N 个)内核线程来为 N 个纤程中的每一个提供服务。这意味着每当一个特定的纤程因为有新的输入数据可用而准备好运行时,我想使用池中的一个内核线程并为纤程提供服务。
    • 如果我将纤程专门绑定到线程,如果纤程的线程在其准备就绪时很忙(但池中的另一个线程可用于运行它),则可能会导致并发性降低。我根本不知道我需要光纤间同步,所以也许 Boost.Coroutine 本身可能是一种选择(据我所知,它确实支持协程跨线程的迁移)。
    猜你喜欢
    • 2019-01-14
    • 2019-12-01
    • 2012-03-30
    • 2011-08-02
    • 1970-01-01
    • 2014-12-16
    • 1970-01-01
    • 2014-06-23
    • 2022-12-05
    相关资源
    最近更新 更多