PHP 协程的实现并非一蹴而就,而是伴随语言内核的演进逐步成熟。从 PHP 5.5 引入 Generator,到 PHP 8.1 正式内置 Fiber,协程能力从“语法糖”走向“原生支持”。理解这条演进路线,需要深入 Zend 引擎的执行栈、内存管理和调度机制。
一、Generator:协程的雏形
PHP 5.5 的 Generator 首次让函数具备“暂停/恢复”能力。其核心在于 Zend 引擎为生成器函数创建了独立的执行上下文(zend_generator),并将原本位于 C 栈上的执行状态迁移到堆内存中。
1.1 执行栈的堆化
普通函数调用时,局部变量和调用信息保存在 C 栈上,函数返回即销毁。生成器函数被调用时,Zend 引擎不会直接执行函数体,而是创建一个 zend_generator 对象,其中包含:
execute_data:指向堆上分配的zend_execute_data,保存当前执行位置、符号表等。stack:一块连续内存,模拟 C 栈用于保存操作数栈和临时变量。node:记录当前执行到的 opcode 位置。
当调用 $gen->current() 或 $gen->next() 时,引擎将 execute_data 切换到该生成器的上下文,从 node 记录的位置继续执行 opcode,遇到 yield 则保存状态并返回。
1.2 yield 的底层语义
yield 在 opcode 层面对应 ZEND_YIELD。执行时,引擎将 yield 的值写入生成器对象的返回值,然后执行 ZEND_GENERATOR_LEAVE,将当前 execute_data 从全局执行栈中摘除,控制权交还给调用者。恢复时,ZEND_GENERATOR_CREATE 重新将 execute_data 压入执行栈。
1.3 Generator 的局限
Generator 只能用于 foreach 或手动迭代,无法实现真正的协程调度。原因在于:
- 生成器函数一旦开始执行,只能通过
yield暂停,无法从外部强制中断。 - 没有独立的调度器,多个生成器之间无法自动切换。
- 无法在任意函数调用层级中暂停,
yield只能出现在生成器函数体内。
因此,Generator 更适合称为“可中断的迭代器”,而非完整协程。
二、从 Generator 到协程的桥梁:yield from 与调度器
PHP 7.0 引入 yield from,允许生成器委托另一个生成器或可迭代对象。这为协程调度器提供了语法基础。
社区方案如 Amp、Recoil 利用 yield 作为暂停点,配合事件循环实现协程调度。其原理是:
- 协程函数返回一个 Generator。
- 调度器不断调用
$gen->current(),获取 yield 出的“指令”(如等待 I/O)。 - 调度器注册 I/O 事件,当事件就绪时调用
$gen->send($result)恢复协程。
这种模式本质上是用户态调度:协程的暂停和恢复由用户代码控制,Zend 引擎只提供 Generator 的暂停能力。问题在于:
- 所有协程代码必须显式
yield,无法在普通函数中暂停。 - 异常传播、栈追踪复杂,调试困难。
- 性能开销大,每次切换涉及 Generator 对象的创建和销毁。
三、Fiber:内核级协程
PHP 8.1 引入 Fiber,标志着协程能力正式进入内核。Fiber 不再依赖 Generator 的迭代语义,而是提供独立的执行栈和切换原语。
3.1 Fiber 的底层结构
Fiber 在 Zend 引擎中对应 zend_fiber 结构体,关键字段包括:
stack:独立的内存栈,大小可配置(默认 4KB 起,按需增长)。context:保存 CPU 寄存器状态(如rip、rsp、rbp),用于上下文切换。caller:指向调用者 Fiber 或主执行栈。status:状态机,包括RUNNING、SUSPENDED、TERMINATED等。
与 Generator 不同,Fiber 的栈是真正的独立栈,而非模拟栈。这意味着 Fiber 可以在任意函数调用层级中暂停,包括嵌套函数调用。
3.2 上下文切换机制
Fiber 的切换通过 zend_fiber_switch_context 实现,核心是汇编级的上下文保存与恢复:
// 简化示意
void zend_fiber_switch_context(zend_fiber_context *from, zend_fiber_context *to) {
// 保存当前寄存器到 from->context
// 从 to->context 恢复寄存器
// 切换栈指针
}
在 x86-64 上,这通常通过 swapcontext 或自定义汇编实现。切换时,当前 Fiber 的寄存器状态被保存到 context,然后加载目标 Fiber 的寄存器状态,栈指针切换到目标栈。整个过程在内核态完成,无需用户代码干预。
3.3 调度模型
Fiber 本身不提供调度器,它只提供“暂停/恢复”原语。调度逻辑仍由用户态实现,但相比 Generator 有本质区别:
- 任意暂停:
Fiber::suspend()可在任何函数中调用,无需函数是生成器。 - 独立栈:每个 Fiber 有独立栈,局部变量、调用链完整保留。
- 异常传播:异常可在 Fiber 间传递,栈追踪完整。
典型调度器如 Revolt 事件循环,利用 Fiber::suspend() 暂停协程,事件就绪后通过 Fiber::resume() 恢复。由于 Fiber 的切换在内核完成,性能远优于 Generator 方案。
3.4 与 Generator 的性能对比
| 特性 | Generator | Fiber |
|---|---|---|
| 栈 | 模拟栈(堆) | 独立栈(内存) |
| 暂停点 | 仅 yield | 任意位置 |
| 切换开销 | 较高(对象操作) | 较低(寄存器切换) |
| 调度 | 用户态迭代 | 用户态 + 内核原语 |
| 异常传播 | 受限 | 完整 |
四、内核演进的本质
从 Generator 到 Fiber,PHP 协程的演进本质是执行栈管理权的转移:
- Generator 阶段:Zend 引擎将函数执行状态堆化,但栈结构仍是模拟的,暂停点受限。
- Fiber 阶段:Zend 引擎直接管理独立栈和 CPU 上下文,暂停点任意,切换由内核原语完成。
这一演进使 PHP 协程从“语法技巧”变为“一等公民”。Fiber 的引入也为 Swoole、RoadRunner 等常驻内存框架提供了原生协程支持,减少了扩展层面的 hack。
五、总结
PHP 协程的实现经历了从 Generator 的“模拟栈 + yield 暂停”到 Fiber 的“独立栈 + 上下文切换”的演进。Generator 解决了“可中断迭代”问题,但受限于模拟栈和显式 yield;Fiber 通过内核级上下文切换,实现了任意位置暂停和完整栈保留,为 PHP 协程调度提供了坚实基础。理解这一演进,有助于在选型时判断:简单迭代用 Generator,高并发协程调度用 Fiber。
未经允许不得转载:任鹏个人博客 » PHP 协程底层实现原理:从 Generator 到 Fiber 的内核演进

