欢迎来到「我是真的狗杂谈世界」,关注不迷路
前言
很早就听说PHP8.1出了Fiber(又称纤程),但一直也没时间捣鼓它,
正好前段时间在整理PHP的新特性/功能,想看看有没有什么可以给日常开发带来便利、安全、性能提升的,再看到它感觉跟性能有点关系,
于是就决定捣鼓一下,整理记录下捣鼓过程。
使用
基本使用
作为开发者(语言使用用户),肯定先感受一下怎么用。按照官方的demo跑了一下:
$fiber = new Fiber(function (): void { echo "我是第二个输出\n"; Fiber::suspend(); echo "我是第四个输出\n";});echo "我是第一个输出\n";$fiber->start();echo "我是第三个输出\n";$fiber->resume();echo "我是第五个输出\n";
我是第一个输出我是第二个输出我是第三个输出我是第四个输出我是第五个输出
看输出顺序不出所料,看demo的意思应该还可以双向传递数据:
$fiber = new Fiber(function (string $fruit1, string $fruit2): void { echo "我是第二个输出;混合水果为:{$fruit1}, {$fruit2}\n"; $amount = Fiber::suspend("{$fruit1}{$fruit2}汁"); echo "我是第四个输出;收银为:{$amount}\n"; Fiber::suspend($amount - 23.5);});echo "我是第一个输出\n";$juice = $fiber->start("苹果", "西瓜");echo "我是第三个输出;果汁为:{$juice}\n";$change = $fiber->resume(50);echo "我是第五个输出;找零为:{$change}\n";
我是第一个输出我是第二个输出;混合水果为:苹果, 西瓜我是第三个输出;果汁为:苹果西瓜汁我是第四个输出;收银为:50我是第五个输出;找零为:26.5
对比生成器
好家伙,看起来是挺新鲜的,但是不是立马想到了PHP5中就支持的生成器+yield(具体生成器+yield的使用说明参考「Chapter 3.PHP5 生成器与yield及原理浅析」
)呢?
于是带着疑问阅读了一下官方文档,果然有写两者的区别:
可以在无需改造中间函数模拟构建调用堆栈的前提下,在任意位置进行Fiber控制,而这在之前的生成器+yield的实现下是无法做到的
这也是之前PHP界一些协程框架一直被诟病之处,必须通过层层嵌套来模拟(模拟方式参考「Chapter 3.PHP5 生成器与yield及原理浅析」
尝试一下(Fiber匿函中调用otherFunc时无需像yield那样就行改造,就像正常函数调用一样就可以):
$fiber = new Fiber(function (): void { otherFunc();});echo "我要开始咯\n";$juice = $fiber->start();echo "另一个函数成功暂停了Fiber;现在尝试恢复Fiber\n";$fiber->resume();function otherFunc(): void{ echo "Fiber已经进入了另一个函数;现在在这里尝试暂停该Fiber\n"; Fiber::suspend(); echo "Fiber已经恢复,我也结束自己的生命周期了\n";}
我要开始咯Fiber已经进入了另一个函数;现在在这里尝试暂停该Fiber另一个函数成功暂停了Fiber;现在尝试恢复FiberFiber已经恢复,我也结束自己的生命周期了
为什么?
知道了可以这样用,总是想要再进一步了解下为什么Fiber可以这么用,而生成器+yield却不行呢?
带着这个问题开始下一个环节:
深入一点
继续在官方文档中寻找蛛丝马迹,发现其实在同一段话中已经把原因也说清楚了~~
原因就是:
- 生成器则无栈
- Fiber具备独立执行堆栈(执行堆栈相关知识参考「Chapter 4. 程序执行过程中的执行堆栈」)
生成器执行过程
生成器的执行过程参考「Chapter 3.PHP5 生成器与yield及原理浅析」
Fiber执行过程
基于Fiber具备独立执行堆栈的前提,不妨以下方代码为例猜想一下Fiber大致执行过程(先不管双向值传递):
$fiber = new Fiber(function (): void { otherFunc();});echo "我要开始咯\n";$juice = $fiber->start();echo "另一个函数成功暂停了Fiber;现在尝试恢复Fiber\n";$fiber->resume();function otherFunc(): void{ echo "Fiber已经进入了另一个函数;现在在这里尝试暂停该Fiber\n"; Fiber::suspend(); echo "Fiber已经恢复,我也结束自己的生命周期了\n";}
上下文信息初始化
- 分配一块空间用于维护主执行堆栈和全部Fiber执行堆栈上下文信息的集合(图中的101~200区块)
图中(下图同)内存只展示用户空间几个主要区块,并且为了更简单展示Fiber的执行过程,内存、CPU以及ZendVM屏蔽和抽象了一些细节,如需更多了解细节,可参考以下文章:
Fiber的初始化
$fiber = new Fiber(function (): void { otherFunc();});
当CPU以主线程栈区为执行堆栈执行到002行时:
- 在用户空间分配一块内存(一般从堆中)充当该Fiber的执行堆栈(图中9900~9801区块)
- 将该Fiber状态以及其执行堆栈上下文信息加入上下文维护集合(图中106~110,可以认为Fiber包的指令起始位置为003行,因此EIP为003;匿函没有参数和局部变量,因此ESP和EBP都指向9900)
Fiber的启动
$juice = $fiber->start();
CPU继续以主线程栈区为执行堆栈执行了005行,执行到006行时:
- 暂存当前CPU上下文至维护集合中主执行堆栈上下文信息中(图中101~105,当前执行指令行为006,因此EIP为007)
- 从维护集合中将目标Fiber标记为激活状态,并将其执行堆栈上下文信息(图中106~110)填充至当前CPU上下文中
Fiber中调用otherFunc函数
CPU以Fiber栈区为执行堆栈执行003行,调用otherFunc函数,函数调用的过程参考「Chapter 4. 程序执行过程中的执行堆栈」
Fiber的暂停
Fiber::suspend();
CPU以Fiber栈区为执行堆栈执行012行,执行到013行时:
- 暂存当前CPU上下文至维护集合中代表该Fiber的上下文信息中(图中106~110,当前执行指令行为013,因此EIP为014;当前Fiber在otherFunc中且无参数和局部变量,因此ESP和EBP指向otherFunc栈帧,也就是9899)
- 将该Fiber标记为休眠状态
- 从维护集合中将主执行堆栈上下文信息(图中101~105)填充至当前CPU上下文中
Fiber的恢复
$fiber->resume();
CPU以主线程栈区为执行堆栈执行007行,执行到008行时(同上述 Fiber的启动过程):
- 暂存当前CPU上下文至维护集合中主执行堆栈上下文信息中(图中101~105,当前执行指令行为008,因此EIP为009)
- 从维护集合中将目标Fiber标记为激活状态,并将其执行堆栈上下文信息(图中106~110)填充至当前CPU上下文中
Fiber的终结
CPU以Fiber栈区为执行堆栈执行014行,otherFuc函数返回(栈帧出栈),回到004行,Fiber终结时:
- 从维护集合中删除目标Fiber的状态信息及其执行堆栈上下文信息(图中106~110)
- 销毁该Fiber对应的执行堆栈(图中9801~9900)
- 将主线程(启动/唤醒该Fiber的调用者)上下文信息(图中101~105)填充至当前CPU上下文中,使CPU回到主线程栈运行
源码验证
以上过程只是猜测,为了验证这个猜测的正确性与正确度,带着三脚猫的C记忆来看一下Fiber的源码实现:
核心代码文件
先找到几个主要文件中的主要代码块(还好PHP源码很容易就能找到这俩文件):
Fiber核心结构
为Fiber设置了初始化、运行中、暂停、死亡四种状态:
typedef enum {ZEND_FIBER_STATUS_INIT,ZEND_FIBER_STATUS_RUNNING,ZEND_FIBER_STATUS_SUSPENDED,ZEND_FIBER_STATUS_DEAD,} zend_fiber_status;
每个Fiber的上下文结构中维护了该Fiber的函数入口、执行堆栈、状态等信息
struct _zend_fiber_context {void *handle;void *kind;zend_fiber_coroutine function;zend_fiber_clean cleanup;zend_fiber_stack *stack;zend_fiber_status status;zend_execute_data *top_observed_frame;void *reserved[ZEND_MAX_RESERVED_RESOURCES];};
一个Fiber结构中包含了自身、调用者、恢复目标的上下文,执行堆栈当前栈底帧。看起来不是独立一块空间维护全部上下文,而是自维护并组成链表
struct _zend_fiber {zend_object std;uint8_t flags;zend_fiber_context context;zend_fiber_context *caller;zend_fiber_context *previous;zend_fcall_info fci;zend_fcall_info_cache fci_cache;zend_execute_data *execute_data;zend_execute_data *stack_bottom;zend_vm_stack vm_stack;zval result;};
Fiber核心功能
为了方便阅读,源码中仅留下重要代码,前后用代替了:
Fiber::start
、Fiber::resume
方法内部指向zend_fiber_resume
函数Fiber::suspend
方法内部指向zend_fiber_suspend
函数
ZEND_METHOD(Fiber, start){fiber->previous = &fiber->context;zend_fiber_transfer transfer = zend_fiber_resume(fiber, NULL, false); }ZEND_METHOD(Fiber, suspend){zend_fiber_transfer transfer = zend_fiber_suspend(fiber, value);}ZEND_METHOD(Fiber, resume){zend_fiber_transfer transfer = zend_fiber_resume(fiber, value, false);}
zend_fiber_resume
、zend_fiber_suspend
函数内部指向zend_fiber_switch_to
函数(顾名思义进行fiber切换)
static zend_always_inline zend_fiber_transfer zend_fiber_resume(zend_fiber *fiber, zval *value, bool exception){zend_fiber_transfer transfer = zend_fiber_switch_to(fiber->previous, value, exception);}static zend_always_inline zend_fiber_transfer zend_fiber_suspend(zend_fiber *fiber, zval *value){return zend_fiber_switch_to(caller, value, false);}
zend_fiber_switch_to
函数内部指向zend_fiber_switch_context
函数(顾名思义切上下文),结合上面定义的Fiber上下文结构,最终应该就是通过VM切换CPU上的寄存器内容了,就到这儿吧(再深入怕胡说八道露了馅)
static zend_always_inline zend_fiber_transfer zend_fiber_switch_to(zend_fiber_context *context, zval *value, bool exception) {zend_fiber_switch_context(&transfer);}
- 还有一个初始化分配Fiber所需空间的函数
static zend_object *zend_fiber_object_create(zend_class_entry *ce){zend_fiber *fiber = emalloc(sizeof(zend_fiber));memset(fiber, 0, sizeof(zend_fiber));}
阅读小结
到了这里,可以知道猜测跟实现略有出入,但基本差不多。
总结与扩展
- 这样看起来,Fiber(纤程)其实就是一种有栈协程(用户态线程)的实现,因此它具备全部协程的特点;
- Fiber本身是一种N:1的线程模型,也许可以结合多线程扩展来实现类似Golang的N:M模型(pthread被放弃了,还未尝试,不过也需要结合第5点);
- Fiber将全部的切换过程完全交由用户(开发者)控制,没有像Golang在runtime或者说引擎/VM中做任何掌控/协助调度的功能;
- Fiber本身并没有解决IO阻塞问题,如果直接用它不会提升效率,反而会带来额外的性能开销和阅读成本;
- 如果想要有所发挥,感觉需要在Cli运行模式下封装非阻塞IO和fiber调度器(react和amp基本都是这思路,只是基于生成器+yield方案),同时用第2点的方式构建N:M模型来避免阻塞调用的影响,但这样基本都构成一套简易Golang的调度runtime(参考「Chapter 9. Go goroutine与其调度过程」);
- 而在FPM运行模式下,感觉有点没什么用武之地,更像是一个底层的玩具API(或许官方在8.1加入Fiber只是第一步,未来可能会从各方面动作来配合实现性能和并发的提升,但势必也意味着复杂和变化吧~)。
最后贴一个Fiber和Swoole的瓜,许久没关注,错过了刀光剑影~
来源地址:https://blog.csdn.net/u011757697/article/details/128256823