下述为开源项目 openppp2(github)构建工作在 C/C++ 17 的 stackful 有栈协同程式的工作流切换示意图:
在 openppp2 之中采用人工手动方式管理协同程式之间的切换,每个中断过程只是保存线程栈信息(如寄存器、当前#PC EIP)并且JMP相对寄存器长跳转到其它协同程序当前EIP位置。
它(协同程式)的切换开销是最小的,速度几乎是无损最快的,这好比人们在 C/C++ 之中用 “函数指针(地址标识符)” 去指向一个 C/C++ 函数,且通过函数指针进行调用,它本身就是长跳转的一种,区别只是少做了线程信息保存,及下个协同程序线程信息恢复的动作。
当然协同程式有一些是通过操作系统核心库API构建的有栈协同程式。
例如:
1、Windows NT平台使用:GetThreadContext、SetThreadContext 函数来构建的有栈协同切换。
2、Linux 平台上使用 ucontext.h 库函数:getcontext、setcontext、makecontext、swapcontext 函数来构建的有栈协同切换。
那么:此类有栈协同程式切换效能的确不行,某些人不要张口就来,不是真懂别乱逼逼。
但通过汇编语言构建的有栈协同程式,几乎不存在性能问题,单线程挂载百万个、千万个协同程式都可以,只要母鸡内存足够大,即可。
stackless 有栈协同程式不要来碰瓷效能,比协同程式切换效能,stackless 真不配跟 stackfull 协同程式比切换效能,若我们不懂这两个协同程式怎么实现的,还真有可能被XX们忽悠住,但可惜我们了解这两类协同程式底层是怎么构建的,当然也自行实现构建过两者,所以在这块还是有一定心得发言权的。
从上述的协同程式示意图之中,可以清晰的看到,在中断某一个协同程式时,会直接切换CPU到下一个协同程式(若有,否则为协同程式结束),而不是还会等待执行到。
但这有一个缺点,一旦某一个协同不按照正确流程切换,那么就会导致协程 deadline 问题,出现致命性故障,如流程中断、内存泄漏等问题层出不穷。
欲兼容性解决这类问题,那么必须引入一个协同程式调度器,但这并非必须的,长长构建多线程并行程式、对于协同程序非常了解的童鞋并不需要,因为这种调度切换的低级问题,不会在它们身上发生。
对于 C/C++ 编程语言来说首先采用 stackful 有栈协同程式,而不是使用 stackless 协同程式,即便是 C/C++ 20 标准提供的 stackless 协同程式,或者早前基于 boost 库提供的 stackless 协同程式。
关于在 go 语言之中,go 开启一个新的 stackless 协同程式,并不意味着立即运行,go 开发人员适用编译器关键字 go 开启新协同程式,能否立即运行取决于以下条件。
在 go 运行时仅只有单线程的情况下,它无法立即运行,而是需要等待当前正在执行协同程序到中断位置或结束时才运行,多线程情况下取决于那个闲置线程先获取到事件(基于系统 epoll、iocp、kqueue 构建的EDSM事件驱动状态机,运行时调度器)。
了解关于我们对于协程相关的一些看法,可以参见本人的以下文章:
C/C++ 如何正确的切换协同程序?(基于协程的并行架构)_c++怎么切换运行程序-CSDN博客
stackless or stackfull 协同程式(协程)?_boost stackless-CSDN博客
灌水玩玩 ChatGPT AIGC生成的有栈协同程序实现(例子)_任务协同 aigc-CSDN博客
C/C++ 11/14/17 有栈式协同程式的基础框架类库【关于】_c++11实现有栈对称协程库-CSDN博客
C++ 20标准协同程序(协程)基于编译器展开的 stackless 协程。-CSDN博客
关于 Go 协同程序(Coroutines 协程)、Go 汇编及一些注意事项。-CSDN博客
童鞋们可以好好理解在这些文章之中,我们关于协同程式的一些看法,那么童鞋们会对于协同程式有更深入的理解,不要去现在鱼龙混杂的逼乎(知乎)上看那些莫名其妙的想法及观点,国内技术相关这块大概就博客园、CSDN博客、看雪论坛比较好一点,其它建议还是算了把。
下述代码为 Linux 平台基于 ucontext.h 函数库实现的有栈协同程式切换(它很简明):
#include <ucontext.h>
#include <iostream>
#include <cstring>
#define STACK_SIZE 1024*64
ucontext_t mainContext, coroContext;
void coroutineFunction() {
std::cout << "Coroutine started" << std::endl;
// 切换回主上下文
swapcontext(&coroContext, &mainContext);
std::cout << "Coroutine resumed" << std::endl;
}
int main() {
char stack[STACK_SIZE];
getcontext(&coroContext);
coroContext.uc_link = &mainContext;
coroContext.uc_stack.ss_sp = stack;
coroContext.uc_stack.ss_size = sizeof(stack);
makecontext(&coroContext, (void (*)())coroutineFunction, 0);
std::cout << "Main started" << std::endl;
// 切换到协程上下文
swapcontext(&mainContext, &coroContext);
std::cout << "Main resumed" << std::endl;
return 0;
}