协程的核心就是上下文切换,故以此为 Swoole 源码分析的开篇。Swoole 与 Fiber 都采用了 boost 库中协程上下文切换( context switch )的实现代码,在这一点上三者完全相同。既然如此,应当先熟悉 boost context 库的使用,然后分析协程上下文切换的汇编实现原理,最后再看 Swoole 中的封装做法,本文就按照这个顺序来写。熟悉 PHP 的同学也可以先阅读一下:PHP 中的协程实现

查阅 boost context 文档

文档传送门:boost Context 文档 。其在简介中写的很清楚,提供了在单线程中进行任务切换的能力,保存了执行流切换与恢复所需的调用栈、寄存器、局部变量等等,可以用来实现协程、为 C++ 提供 yield 关键字之类的功能。 boost context 相关的源码位于 boost / libs / context ,由于 boost 的 github 仓库用到了很多 submodules ,加上众所周知的原因,我一直拉不下来完整的代码(梯子不是特别稳的话最后有些 sbmodules 一直是空的)。没办法,最后是下载 release 的压缩包。以前一直觉得发布源码压缩包没啥用,只有预编译包还有点意义,现在感觉 —— 真香。

协程栈

对于“协程栈”这个词,在不同的地方似乎有不同的理解,在本文姑且认为其指的是协程的嵌套调用。例如当前有 A、B、C、D 四个任务,需要在 A 中调用 B ,在 B 中又调用 C ,等 B 执行完后,又在 A 中调用 D 。如果是所谓的有栈协程,那么调用非常方便,整体是和普通函数调用一样的栈式结构,而调度器则是把这个栈式整体作为调度对象。

sequenceDiagram
    A->>+B: call_coroutine
    B->>+C: call_coroutine
    C->>+D: call_coroutine
    D-->>-C: coroutine_return
    C-->>-B: coroutine_return
    B-->>-A: coroutine_return

图1:有栈协程的嵌套调用

如果是无栈协程,是不能这样嵌套调用的,要实现目的只能两个方案:1:适当安排任务,由 A 启用协程 B (随后 A 结束),类似地再由 B 启用 C 、C 启用 D 的方式,原执行意图的顺序不变,但是代码逻辑复杂不易维护;2:在语言用户层面,利用语言特性自实现协程栈的效果,但这样性能自然格外低。

很明显,无栈协程不太方便。其次,许多资料里面提到无栈协程性能更高,但起码 PHP 中单纯的 yield 形式组织的任务,在性能上是比不上 yield from 的。其次所谓的协程栈的内存拷贝,我个人认为并不存在(除非是协程栈满了需要扩容,但这个情况并不常见)。再其次,本身函数调用就会有栈帧,有栈协程只是把整体作为了调度对象,而无栈协程则几乎是一个低速任务起一个协程,不管具体实现是如何切换的,其上下文状态的保存依旧是跑不掉的。所以我认为,从理论的角度,没有证据表明二者之间明显的性能差距。

boost 协程基础用法

boost 协程双向通信

Swoole 中的协程上下文( context )

Fiber 中的上下文切换

Swoole 协程上下文切换实现原理

linux 上的 AT&T 汇编记法

x64 汇编基础

x64 函数栈帧

swoole_make_fcontext 汇编源码分析

swoole_jump_fcontext 汇编源码分析

思考:为什么要保存所有约定不变寄存器

梳理 Swoole 协程上下文切换整体流程

从 main 进入协程函数

从协程函数 jmp 回 main

从一个协程函数 jmp 到另一个协程函数

从一个协程函数 jmp 出后再 jmp 回去

从一个协程函数 return

分析 swoole::corutine::Context

swoole 协程栈的申请与释放

swap_in() 与 swap_out()

swoole 协程工作函数与参数封装

swap_out() 后,执行流到哪里了

swap_in() 后,执行流从哪里恢复

协程栈如果满了(爆栈)怎么办

boost 中的应对方法

swoole 中的应对方法

总结