Swoole协程之旅-前篇

写在最前

  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后续也会增加这个特性)。

  协程近几年如此火爆,很大一部分原因归功与golang在中国的流行和快速发展,受到很多开发的喜爱。目前支持协程的语言有很多,例如: golang、lua、python、c#、javascript等。大家也可以用很短的代码用c/c++撸出协程的模型。当然PHP也有自己的协程实现,也就是生成器,我们这里不展开讨论。

Swoole1.x

   Swoole最初以高性能网络通讯引擎的姿态进入大家视线,Swoole1.x的编码主要是异步回调的方式,虽然性能非常高效,但很多开发都会发现,随着项目工程的复杂程度增加,以异步回调的方式写业务代码是和人类正常思维相悖的,尤其是回调嵌套多层的时候,不仅开发维护成本指数级上升,而且出错的几率也大幅增加。大家理想的编码方式是:同步编码得到异步非阻塞的效果。所以Swoole很早的时候就开始了协程的探索。

  最初的协程版本是基于PHP生成器Generators\Yield的方式实现的,可以参考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的栈,进而实现协程。

Swoole4.x

4.x协程我们放在最后。

协程之旅前篇结束,下一篇文章我们将深入Zend分析Swoole原生协程PHP部分的实现。