写在最前
??Swoole 协程经历了几个里程碑, 我们需要在前进的道路上不断总结与回顾自己的发展历程, 正所谓温故而知新, 本系列文章将分为协程之旅前, 中, 后三篇.
前篇主要介绍协程的概念和 Swoole 几个版本协程实现的主要方案技术; 中篇主要深入 Zend 分析 PHP 部分的原理和实现; 后篇主要补充和分析协程 4.x 的实现.
软文正式开始
协程是什么?
?? 概念其实很早就出现了, 摘 wiki 一段: According to Donald Knuth, the term coroutine was coined by Melvin Conway in 1958, after he applied it to construction of an assembly program.The first published explanation of the coroutine appeared later, in 1963. 协程要比 c 语言的历史还要悠久, 究其概念, 协程是子程序的一种, 可以通过 yield 的方式转移程序控制权, 协程之间不是调用者与被调用者的关系, 而是彼此对称, 平等的. 协程完全有用户态程序控制, 所以也被成为用户态的线程. 协程由用户以非抢占的方式调度, 而不是操作系统. 正因为如此, 没有系统调度上下文切换的开销, 协程有了轻量, 高效, 快速等特点.(大部分为非抢占式, 但是, 比如 golang 在 1.4 也加入了抢占式调度, 其中一个协程发生死循环, 不至于其他协程被饿死. 需要在必要的时刻让出 CPU,Swoole 在 V4.3.2 增加了这个特性).
?? 协程近几年如此火爆, 很大一部分原因归功与 golang 在中国的流行和快速发展, 受到很多开发的喜爱. 目前支持协程的语言有很多, 例如: golang,lua,python,c#,JavaScript 等. 大家也可以用很短的代码用 c/c++ 撸出协程的模型. 当然 PHP 也有自己的协程实现, 也就是生成器, 我们这里不展开讨论.
Swoole1.x
?? Swoole 最初以高性能网络通讯引擎的姿态进入大家视线, Swoole1.x 的编码主要是异步回调的方式, 虽然性能非常高效, 但很多开发都会发现, 随着项目工程的复杂程度增加, 以异步回调的方式写业务代码是和人类正常思维相悖的, 尤其是回调嵌套多层的时候, 不仅开发维护成本指数级上升, 而且出错的几率也大幅增加. 大家理想的编码方式是: 同步编码得到异步非阻塞的性能. 所以 Swoole 很早的时候就开始了协程的探索.
?? 最初的协程版本是基于 PHP 生成器 GeneratorsYield 的方式实现的, 可以参考 PHP 大神 Nikita 的早期博客的关于协程介绍. PHP 和 Swoole 的事件驱动的结合可以参考腾讯出团队开源的 TSF 框架, 我们也在很多生产项目中使用了该框架, 确实让大家感受到了, 以同步编程的方式写异步代码的快感, 然而, 现实总是很残酷, 这种方式有几个致命的缺点:
所有主动让出的逻辑都需要 yield 关键字. 这会给程序员带来极大的概率犯错, 导致大家对协程的理解转移到了对 Generators 语法的原理的理解. 由于语法无法兼容老的项目, 改造老的项目工程复杂度巨大, 成本太高.
这样使得无论新老项目, 使用都无法得心应手.
Swoole2.x
?? 2.x 之后的协程都是基于内核原生的协程, 无需 yield 关键字. 2.0 的版本是一个非常重要的里程碑, 实现了 PHP 的栈管理, 深入 zend 内核在协程创建, 切换以及结束的时候操作 PHP 栈. 在 Swoole 的文档中也介绍了很多关于每个版本实现的细节, 我们这篇文章只对每个版本的协程驱动技术做简单介绍. 原生协程都有对 PHP 栈的管理, 后续我们会单独拿一片文章来深入分析 PHP 栈的管理和切换.
?? 2.x 主要使用了 setjmp/longjmp 的方式实现协程, 很多 C 项目主要采用这种方式实现 try-catch-finally, 大家也可以参考 Zend 内核的用法. setjmp 的首次调用返回值是 0,longjmp 跳转时, setjmp 的返回值是传给 longjmp 的 value. setjmp/longjmp 由于只有控制流跳转的能力. 虽然可以还原 PC 和栈指针, 但是无法还原栈帧, 因此会出现很多问题. 比如 longjmp 的时候, setjmp 的作用域已经退出, 当时的栈帧已经销毁. 这时就会出现未定义行为. 假设有这样一个调用链:
func0() -> func1() -> ... -> funcN()
只有在 func{i}()中 setjmp, 在 func{i+k}()中 longjmp 的情况下, 程序的行为才是可预期的.
Swoole3.x
3.x 是生命周期很短的一个版本, 主要借鉴了 fiber-ext 项目, 使用了 php7 的 VM interrupts 机制, 该机制可以在 vm 中设置标记位, 在执行一些指令的时候 (例如: 跳转和函数调用等) 检查标记位, 如果命中就可以执行相应的 hook 函数来切换 vm 的栈, 进而实现协程. 虽然我们完整的实现了协程的功能, 但是由于并没有相对 2.x 有很大的进步, 原因我们后续的文章会做进一步分析, 所以我们放弃了这个版本, 直接进入了 4.x 的版本迭代.
Swoole4.x
4.x 协程是当前 Swoole 的协程版本, 借鉴了前面版本的缺点和问题, 引入了 PHP+C 双栈管理维护, 完美的支持 PHP 各种语法, 详细分析我们放在系列文章最后.
End
协程之旅前篇结束, 下一篇文章我们将深入 Zend 分析 Swoole 原生协程 PHP 部分的实现.
来源: https://www.2cto.com/kf/201904/804350.html