C/C++ 协同程式切换存在以下几个潜在风险:
1、托管资源泄漏(注意:只要是人为显示管理的资源,均为托管资源)
2、协程切换被挂起(永久/等于 deadline 死亡)
3、平台兼容性问题
4、多核编程临界安全问题
以 stackful 有栈协同程式为例,本人不喜欢 C++ 20 的 stackless 编译器支援协同程式,因为麻烦且不好用、而且还有编译器标准版本最小限制。
人们可先行参考本人的这些文章:
C++ 20标准协同程序(协程)基于编译器展开的 stackless 协程。_协同编译具体-CSDN博客
关于 Go 协同程序(Coroutines 协程)、Go 汇编及一些注意事项。-CSDN博客
stackless or stackfull 协同程式(协程)?_boost stackless-CSDN博客
C/C++ 11/14/17 有栈式协同程式的基础框架类库【关于】_c++17 协程-CSDN博客
C/C++ 如何正确的切换协同程序?(基于协程的并行架构)_c++怎么切换运行程序-CSDN博客
关于 C/C++ 1Z(17)开源项目 openppp2 协同程式切换工作流-CSDN博客
C/C++ setjmp 与 longjmp 函数的实现_insthookproc-CSDN博客
人们可自行研究基于 C/C++ 11/14/17 协同程序架构的开源项目:openppp2,它托管在 github 开源仓库上面。
在 Android 平台上面,使用有栈协同程序,无法调用 socket、connect 函数,否则会导致程序直接崩溃,但人们可以委托到由 std::thread 创建的线程上异步完成 socket、connect 函数的调用,在异步回调给协同程序。
托管资源泄漏常见于,协程切换出现故障,在多线程下面,它更容易出现难以预料的未知错误,但为了追求效能,我们通常希望尽可能的并行驱动所有可执行的协同程式。
首先协程的切换流程为:
A协程让出CPU(Yield behavior)长跳到B协程的当前EIP(#PC寄存器)代码执行位置开始执行。
但在这个过程之中,协程可能没有切换完毕之前,A协程调用的异步函数,已经完成需要A重新恢复执行,而A如果没有内部状态机,那么就会导致程序发生崩溃,为了安全性每个协同程序都一个内部原子状态机,当 resume 时,A的状态并没有被完成标记 Yield,那么以 C/C++ 程序为了健壮性,通常会忽略不会抛出错误,这样就出现泄漏问题了,因为A协同程序不会再被恢复运行。
假设:A已经完成了标记 yield,这个时候在resume,也会存在致命的风险性,因为标记 yield 是切换线程上下文之前,而不是之后,这个时候被 resume、跳转无外乎以下两个结局:
1、协程堆栈被破坏(变花猪)
2、程序直接崩溃
根据我们的长时大规模的测试,崩溃概率很低,大多数情况是堆栈被破坏,产生难以预料的运行时算术错误。
所以:人们需要确保协同程序要切换到对应位置之后才能干其它的事情,而每个协程内部的原子状态机必须存在四个状态。
1、正在挂起(切换前)
2、已经挂起(切换后)
3、正在恢复(恢复前)
4、已经恢复(恢复后)
而恢复函数考虑到多核并行编程的支援,为了确保安全性,必须考虑正在挂起状态还没有设置的情况,所以需要异步循环等待已经挂起被标记,这可以防止协同程序在挂起之前被执行。
单个协同程序,不能在同一时间轮片内被多个线程执行,否则协程通常会直接产生崩溃的致命问题,取决于协同程序的实现架构。
一定是正在挂起状态之前,循环等待已经挂起状态达成后,才能执行 resume(协程恢复行为),当然如果正在挂起,也需要循环等待到已经挂起状态达成后。
但这里循环等待必须是异步事件队列循环,而不是 while 之流,因为它的恢复点或是在当前线程上面执行,如果同步循环就直接永久性卡死了,这跟人为制造死锁没有区别。
通常异步环回一到五次,即可正确处理协同程序挂起/恢复流程,因为切换协同程序的效率是很高的,所以不用担心会挤兑大量的CPU资源。
无论多线程还是单线程上执行协同程序,都需要确保 yield、resume 是成对正确执行,否则资源泄漏就不可避免,同时也可能导致EDSM事件驱动状态机队列发生底层死锁问题。
在无论多核/单核执行的情况下,都可能存在异步回调被提前执行,但协同程序还没有完成 yield 让出 CPU 的情况,相信常年编写异步程序的童鞋是深有体悟。
在单线程之中,可能因为异步内部操作失败而直接调用回调函数,而这个时候协程调用的异步函数仍旧没有返回之前,在多线程之中可能因为异步操作已经完成而直接调用回调函数,而驱动协程的线程处于被操作系统内核挂起的情况,(还没有该线程的时间轮片,调度导致)都可能导致协程先执行 resume 的行为,而协程还没有 yield 让出CPU(或正在挂起之中)。
比如:
A协同程序内部发起一个异步操作,并且 yield 让出CPU,但传入回调函数到异步操作内,异步操作失败,直接丢弃异步操作的上下文,而不驱动异步回调函数的调用,导致协同程序根本无法 resume,有非常多煞笔、智障、且偷懒,不是个人的东西,就很喜欢这么整,这种人是需要深恶痛绝的,即便是异步编程这样的下三滥行为都是不被允许的,试问:谁家会这么教育人这么干?
协同程序可以在多个线程上被驱动,但协同程序内必须是线性执行的,即:A在1线程执行并让出CPU后,可在2线程执行恢复。
这样的多核并行驱动架构,可以显著的提高整个应用系统的吞吐能力,至于多线程带来的复杂性,这并不可怕,合适的架构并不会导致这些致命性问题的产生。
就像在 go 之中,线程由 go runtime 运行时进行管理,go 协同程序可以在不同的线程上被正确执行,C/C++ 也是可以的,go 只不过是简化了这一个过程,并不值得吹嘘,都是套壳 C/C++ 的东西,反过来说比 C/C++ 更强大,那就纯扯淡。
C/C++ 要求开发人员按照工业化标准进行设计及编程,而 C 语言对开发人员并没有这样的工程性质要求,所以在 C++ 之中,开发人员必须遵循详细的条条款款,而不是你想怎么干就怎么干,就像 C 语言,真正的 C++ 开发人员用起来只会感到痛苦是一样的,反过来 C 语言开发人员用 C++ 也会感到痛苦。