stackless 协程与 stackful 协程有何不同?
How do stackless coroutines differ from stackful coroutines?
背景:
我问这个是因为我目前有一个应用程序有很多(成百上千个)线程。这些线程中的大多数大部分时间都处于空闲状态,等待将工作项放入队列中。当一个工作项可用时,它会通过调用一些任意复杂的现有代码来处理。在某些操作系统配置上,应用程序会遇到控制最大用户进程数的内核参数,因此我想尝试减少工作线程数的方法。
我建议的解决方案:
这似乎是一种基于协程的方法,我用协程替换每个工作线程,这似乎有助于实现这一点。然后我可以有一个由实际(内核)工作线程池支持的工作队列。当一个项目被放置在一个特定协程的队列中进行处理时,一个条目将被放置到线程池的队列中。然后它会恢复相应的协程,处理其排队的数据,然后再次挂起,释放工作线程来做其他工作。
实施细节:
在考虑如何执行此操作时,我无法理解无堆栈协程和堆栈协程之间的功能差异。我有一些使用 Boost.Coroutine 库使用堆栈协程的经验。我发现从概念层面上理解起来相对容易:对于每个协程,它都维护一份 CPU 上下文和堆栈,当你切换到协程时,它会切换到保存的上下文(就像内核一样-模式调度程序会)。
我不太清楚的是无堆栈协程与此有何不同。在我的应用程序中,与上述工作项排队相关的开销非常重要。我见过的大多数实现,比如 the new CO2 library 都表明无堆栈协程提供的上下文切换开销要低得多。
因此,我想更清楚地了解stackless和stackful协程之间的功能差异。具体来说,我想到了这些问题:
References like this one 表明区别在于您可以 yield/resume 在堆栈协程和无堆栈协程中的位置。是这样吗?是否有一个简单的例子说明我可以在堆栈协程中执行但在无堆栈协程中不能执行的操作?
自动存储变量(即变量"on the stack")的使用有什么限制吗?
我可以从无堆栈协同程序调用哪些函数有任何限制吗?
如果stackless协程没有保存stack context,那么当协程是运行时自动存储变量到哪里去了?
首先,感谢您浏览CO2 :)
Boost.Coroutinedoc很好的描述了stackful协程的优点:
stackfulness
In contrast to a stackless coroutine a stackful coroutine
can be suspended from within a nested stackframe. Execution resumes at
exactly the same point in the code where it was suspended before. With
a stackless coroutine, only the top-level routine may be suspended.
Any routine called by that top-level routine may not itself suspend.
This prohibits providing suspend/resume operations in routines within
a general-purpose library.
first-class continuation
A first-class continuation can be passed as
an argument, returned by a function and stored in a data structure to
be used later. In some implementations (for instance C# yield) the
continuation can not be directly accessed or directly manipulated.
Without stackfulness and first-class semantics, some useful execution
control flows cannot be supported (for instance cooperative
multitasking or checkpointing).
这对你意味着什么?例如,假设您有一个接受访问者的函数:
template<class Visitor>
void f(Visitor& v);
你想转为iterator,用stackful协程,你可以:
asymmetric_coroutine<T>::pull_type pull_from([](asymmetric_coroutine<T>::push_type& yield)
{
f(yield);
});
但是使用stackless协程,就没办法了:
generator<T> pull_from()
{
// yield can only be used here, cannot pass to f
f(???);
}
总的来说stackful协程比stackless协程更强大。
那么我们为什么要stackless协程呢?简短回答:效率。
Stackful 协程通常需要分配一定数量的内存来容纳其运行时堆栈(必须足够大),并且与无堆栈协程相比,上下文切换更昂贵,例如Boost.Coroutine 需要 40 个周期,而 CO2 在我的机器上平均只需要 7 个周期,因为无堆栈协程唯一需要恢复的是程序计数器。
也就是说,有了语言支持,只要协程中没有递归,stackful 协程可能也可以利用编译器计算的堆栈最大大小,因此内存使用也可以得到改善。
说到stackless协程,记住这并不意味着完全没有runtime-stack,它只是意味着它使用与主机端相同的runtime-stack,所以你可以调用递归函数同样,只是所有递归都将发生在主机的运行时堆栈上。相反,使用堆栈式协程时,当您调用递归函数时,递归将发生在协程自己的堆栈上。
回答问题:
- 自动存储变量的使用有什么限制吗
(即变量 "on the stack")?
没有。这是CO2的仿真限制。有了语言支持,协程可见的自动存储变量将被放置在协程的内部存储中。请注意我强调 "visible to the coroutine",如果协程调用一个在内部使用自动存储变量的函数,那么这些变量将被放置在运行时堆栈中。更具体地说,stackless 协程只需要保留恢复后可以使用的 variables/temporaries。
说清楚一点,你也可以在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
.
- 我可以从
无堆栈协程?
没有
- 如果没有为无堆栈协程保存堆栈上下文,
协程运行时自动存储变量去哪里
运行?
上面已经回答了,stackless协程不关心被调用函数中使用的自动存储变量,它们只会被放在正常的运行时堆栈上。
如果您有任何疑问,只需查看 CO2 的源代码,它可能会帮助您了解引擎盖下的机制 ;)
你想要的是用户空间 threads/fibers - 通常你想在一个深层嵌套的调用堆栈中暂停你的代码(运行 在光纤中)(例如解析来自 TCP 连接的消息).在这种情况下,您不能使用无堆栈上下文切换(应用程序堆栈在无堆栈协程之间共享 -> 被调用子例程的堆栈帧将被覆盖)。
您可以使用类似 boost.fiber 的东西,它基于 boost.context 实现用户空间 threads/fibers。
背景:
我问这个是因为我目前有一个应用程序有很多(成百上千个)线程。这些线程中的大多数大部分时间都处于空闲状态,等待将工作项放入队列中。当一个工作项可用时,它会通过调用一些任意复杂的现有代码来处理。在某些操作系统配置上,应用程序会遇到控制最大用户进程数的内核参数,因此我想尝试减少工作线程数的方法。
我建议的解决方案:
这似乎是一种基于协程的方法,我用协程替换每个工作线程,这似乎有助于实现这一点。然后我可以有一个由实际(内核)工作线程池支持的工作队列。当一个项目被放置在一个特定协程的队列中进行处理时,一个条目将被放置到线程池的队列中。然后它会恢复相应的协程,处理其排队的数据,然后再次挂起,释放工作线程来做其他工作。
实施细节:
在考虑如何执行此操作时,我无法理解无堆栈协程和堆栈协程之间的功能差异。我有一些使用 Boost.Coroutine 库使用堆栈协程的经验。我发现从概念层面上理解起来相对容易:对于每个协程,它都维护一份 CPU 上下文和堆栈,当你切换到协程时,它会切换到保存的上下文(就像内核一样-模式调度程序会)。
我不太清楚的是无堆栈协程与此有何不同。在我的应用程序中,与上述工作项排队相关的开销非常重要。我见过的大多数实现,比如 the new CO2 library 都表明无堆栈协程提供的上下文切换开销要低得多。
因此,我想更清楚地了解stackless和stackful协程之间的功能差异。具体来说,我想到了这些问题:
References like this one 表明区别在于您可以 yield/resume 在堆栈协程和无堆栈协程中的位置。是这样吗?是否有一个简单的例子说明我可以在堆栈协程中执行但在无堆栈协程中不能执行的操作?
自动存储变量(即变量"on the stack")的使用有什么限制吗?
我可以从无堆栈协同程序调用哪些函数有任何限制吗?
如果stackless协程没有保存stack context,那么当协程是运行时自动存储变量到哪里去了?
首先,感谢您浏览CO2 :)
Boost.Coroutinedoc很好的描述了stackful协程的优点:
stackfulness
In contrast to a stackless coroutine a stackful coroutine can be suspended from within a nested stackframe. Execution resumes at exactly the same point in the code where it was suspended before. With a stackless coroutine, only the top-level routine may be suspended. Any routine called by that top-level routine may not itself suspend. This prohibits providing suspend/resume operations in routines within a general-purpose library.
first-class continuation
A first-class continuation can be passed as an argument, returned by a function and stored in a data structure to be used later. In some implementations (for instance C# yield) the continuation can not be directly accessed or directly manipulated.
Without stackfulness and first-class semantics, some useful execution control flows cannot be supported (for instance cooperative multitasking or checkpointing).
这对你意味着什么?例如,假设您有一个接受访问者的函数:
template<class Visitor>
void f(Visitor& v);
你想转为iterator,用stackful协程,你可以:
asymmetric_coroutine<T>::pull_type pull_from([](asymmetric_coroutine<T>::push_type& yield)
{
f(yield);
});
但是使用stackless协程,就没办法了:
generator<T> pull_from()
{
// yield can only be used here, cannot pass to f
f(???);
}
总的来说stackful协程比stackless协程更强大。 那么我们为什么要stackless协程呢?简短回答:效率。
Stackful 协程通常需要分配一定数量的内存来容纳其运行时堆栈(必须足够大),并且与无堆栈协程相比,上下文切换更昂贵,例如Boost.Coroutine 需要 40 个周期,而 CO2 在我的机器上平均只需要 7 个周期,因为无堆栈协程唯一需要恢复的是程序计数器。
也就是说,有了语言支持,只要协程中没有递归,stackful 协程可能也可以利用编译器计算的堆栈最大大小,因此内存使用也可以得到改善。
说到stackless协程,记住这并不意味着完全没有runtime-stack,它只是意味着它使用与主机端相同的runtime-stack,所以你可以调用递归函数同样,只是所有递归都将发生在主机的运行时堆栈上。相反,使用堆栈式协程时,当您调用递归函数时,递归将发生在协程自己的堆栈上。
回答问题:
- 自动存储变量的使用有什么限制吗 (即变量 "on the stack")?
没有。这是CO2的仿真限制。有了语言支持,协程可见的自动存储变量将被放置在协程的内部存储中。请注意我强调 "visible to the coroutine",如果协程调用一个在内部使用自动存储变量的函数,那么这些变量将被放置在运行时堆栈中。更具体地说,stackless 协程只需要保留恢复后可以使用的 variables/temporaries。
说清楚一点,你也可以在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
.
- 我可以从 无堆栈协程?
没有
- 如果没有为无堆栈协程保存堆栈上下文, 协程运行时自动存储变量去哪里 运行?
上面已经回答了,stackless协程不关心被调用函数中使用的自动存储变量,它们只会被放在正常的运行时堆栈上。
如果您有任何疑问,只需查看 CO2 的源代码,它可能会帮助您了解引擎盖下的机制 ;)
你想要的是用户空间 threads/fibers - 通常你想在一个深层嵌套的调用堆栈中暂停你的代码(运行 在光纤中)(例如解析来自 TCP 连接的消息).在这种情况下,您不能使用无堆栈上下文切换(应用程序堆栈在无堆栈协程之间共享 -> 被调用子例程的堆栈帧将被覆盖)。
您可以使用类似 boost.fiber 的东西,它基于 boost.context 实现用户空间 threads/fibers。