欢迎来到《我是真狗说天下》,注意不要迷路,不过一直没时间去鼓捣。正好前段时间在整理PHP的新特性/功能。想看看有没有什么可以给日常开发带来方便、安全、性能提升的。后来看了觉得跟性能有关系,于是决定自己去fiddle,把fiddler的过程整理记录下来。使用基本用法作为开发者(语言使用者),首先要感受一下如何使用。按照官方demo运行:$fiber=newFiber(function():void{echo"我是第二个输出\n";Fiber::suspend();echo"我是第四个输出\n";});echo"我是第一个输出\n";$fiber->start();echo"我是第三个输出\n";$fiber->resume();echo"我是第五个输出\n";我是第一输出,我是第二输出,我是第三输出,我是第四输出,我是第五输出。输出顺序符合预期,demo的意思应该没问题双向数据传输:$fiber=newFiber(function(string$fruit1,string$fruit2):void{echo"我是第二个输出;混合水果是:{$fruit1},{$fruit2}\n";$amount=Fiber::suspend("{$fruit1}{$fruit2}juice");echo"我是第四个输出;收银机是:{$amount}\n";Fiber::suspend($amount-23.5);});echo"我是第一个输出\n";$juice=$fiber->start("apple","watermelon");echo"我是第三个输出;果汁是:{$juice}\n";$change=$fiber->resume(50);echo"我是第五个输出;找零是:{$change}\n";我是第一个输出,我是第二个输出;混合水果是:苹果、西瓜我是第三输出;果汁是:苹果西瓜汁我是第四输出;听上去挺新鲜的,但是没有第一时间想到PHP5中支持的generator+yield(具体的generator+yield说明请参考《第3章PHP5生成器与yield原理分析》")呢子布?于是带着疑惑看了下官方文档,两者的区别是:Fiber控制可以在任意位置进行,无需修改中间函数来模拟调用栈的构建,而这是在前面的generator+yield下面是做不到的,这也是PHP界一些协程框架之前被诟病的地方,必须通过层层嵌套来模拟,在Fiber隐藏函数中调用otherFunc时,有不需要像yield那样改造,就像普通的函数调用一样):$fiber=newFiber(function():void{otherFunc();});echo"I'mgoingtostart\n";$juice=$fiber->start();echo"另一个函数成功暂停了光纤;现在尝试恢复Fiber\n";$fiber->resume();functionotherFunc():void{echo"Fiber已经进入另一个函数;现在尝试在此处暂停Fiber\n";Fiber::suspend();echo"TheFiberhasresumedandIhaveendedmyownlifecycle\n";}IamabouttostarttheFiberalreadyEnteredanotherfunction;现在尝试在这里挂起Fiber另一个函数挂起Fiber成功,现在尝试恢复FiberFiber已经恢复了,我已经结束了自己的生命周期。为什么?我知道可以这样用,我一直想去furtherFindoutwhyFibercanbeusedthisway,generator+yieldcan't?带着这个问题开始下一个链接:深入下去,继续在官方文档中寻找线索,发现已经说清楚原因了同段~~原因是:generator没有栈,Fiber有独立的执行栈(关于执行栈的知识,请参考《第四章程序执行时的执行栈》)。对于生成器的执行过程生成器,参考《第三章PHP5生成器解析及生成器》ditsprinciple”Fiber的执行过程是以Fiber拥有独立的执行栈为前提的。我们以下面的代码为例,猜测一下Fiber的大致执行流程(不考虑双向传值):$fiber=newFiber(function():void{otherFunc();});echo"I'mgoingtostart\n";$juice=$fiber->start();echo"AnotherfunctionsuccessfullypausedFiber;nowtrytoresumeFiber\n";$fiber->resume();functionotherFunc():void{echo"Fiber已经进入另一个函数;现在尝试在这里挂起这个Fiber\n";纤维::暂停();echo"Fiber已经恢复,我也终止了我自己的生命周期结束了\n";}上下文信息初始化分配了一个空间来维护主执行栈和所有Fiber执行栈上下文信息的集合(inblocks101to200图)。图中的内存(下图同)只展示了用户空间的几个主要块,为了更方便的展示Fiber的执行过程,内存、CPU、ZendVM屏蔽和抽象了一些细节.更详细的内容可以参考以下文章:《第四章程序执行过程Execution栈》《第五章从CPU和内存角度看程序运行》Fiber初始化$fiber=newFiber(function():void{otherFunc();});当CPU使用主线程栈区作为执行栈执行到002行时:在用户空间(通常从堆中)分配一块内存作为Fiber的执行栈(图中9900-9801块))将Fiber状态及其执行栈上下文信息加入上下文维护集(图中106-9801)110,可以认为Fiber包指令的起始位置是003行,所以EIP为003;匿名函数没有参数和局部变量,所以ESP和EBP都指向9900)Fiberstart$juice=$fiber->start();CPU继续以主线程栈区作为执行栈执行005行。执行006行时:将当前CPU上下文暂存在维护集中主执行栈的上下文信息中(图中101-105,当前执行指令行为为006,所以EIP为007)将目标纤程标记为从维护集中激活,将其执行栈上下文信息(图中106-110)填充到当前CPU上下文中的fiber中调用otherFunc函数CPU以fiber栈区作为执行栈003行执行,调用otherFunc函数,函数调用流程参考《第4章程序执行时的执行栈》FibersuspendFiber::suspend();CPU以fiber栈区为执行栈执行012行,执行到013行时:将当前CPU上下文暂存到维护集表示Fiber的上下文信息(图中106-110,当前执行指令behavior为013,所以EIP为014;当前Fiber在otherFunc中,没有参数和局部变量,所以ESP和EBP指向otherFunc栈帧,也就是9899)将Fiber标记为休眠状态,填充main当前CPU上下文中从维护集到恢复Fiber的执行栈上下文信息(图中101-105)$fiber->resume();CPU使用主线程栈区作为执行栈执行007行,当执行到008行时(同上Fiber启动过程):将当前CPU上下文暂存到维护集中的主执行栈上下文信息中(图中101-105,当前执行指令行为为008,所以EIP为009)将目标Fiber从维护集中标记为active,将其执行栈上下文信息(图中106-110)填充到最后当前CPU上下文中的Fiber。CPU使用Fiber栈区作为执行栈执行014行,其他Fuc函数返回(弹出栈帧),返回004行。当Fiber终止时:删除目标Fiber的状态信息及其执行从维护集(图中106-110)栈上下文信息,销毁Fiber对应的执行栈(图中9801~9900)填充主线程(启动/唤醒Fiber的调用者)的上下文信息)(图中101~105)进入当前CPU上下文,让CPU返回主线程栈运行源代码校验。以上过程只是猜测。为了验证这个猜测的正确性和正确性,先看一下三足猫C内存的Fiber源码实现:核心代码文件先在几个主文件中找到主要代码块(幸好PHP源代码很容易找到这两个文件):zend_fibers.hzend_fibers.cFiber核心结构为Fiber设置了四种状态:初始化、运行、暂停和死亡:阿图斯;每个Fiber的context结构维护了Fiber的函数入口、执行栈、状态等信息struct_zend_fiber_context{/*指向boost.context或ucontext_t数据的指针。*/void*句柄;/*标识光纤类型的指针。*/void*种类;/*纤程的入口点函数。*/zend_fiber_coroutine函数;/*纤维的清理功能。*/zend_fiber_clean清理;/*分配的C堆栈。*/zend_fiber_stack*堆栈;/*光纤状态。*/zend_fiber_status状态;/*观察者状态*/zend_execute_data*top_observed_frame;/*为扩展保留*/void*reserved[ZEND_MAX_RESERVED_RESOURCES];};一个Fiber结构包含自身、调用者和恢复目标的上下文,以及当前执行栈底帧看起来不是一个独立的空间来维护所有的上下文,而是自己维护的,由链表组成/**/struct_zend_fiber{/*PHP对象句柄。*/zend_object标准;/*标志在枚举zend_fiber_flag中定义。*/uint8_t标志;/*本机C纤程上下文。*/zend_fiber_context上下文;/*恢复我们的纤维。*/zend_fiber_context*调用者;/*暂停我们的纤维。*/zend_fiber_context*previous;/*光纤启动时使用的回调和信息/缓存。*/zend_fcall_infofci;zend_fcall_info_cachefci_cache;/*当前ZendVM执行纤程正在运行的数据。*/zend_execute_data*execute_data;/*fibervm堆栈底部的框架。*/活动zend_execute_data*stack_bottom;光纤虚拟机堆栈。*/zend_vm_stackvm_stack;/*纤维返回值的存储。*/zval结果;};为了阅读方便,Fiber的核心函数在源码中只留下重要代码,前后用/.../替换:Fiber::start,Fiber::resume方法内部指向zend_fiber_resume函数Fiber::suspend方法内部指向zend_fiber_suspend函数ZEND_METHOD(Fiber,start){/*...*/fiber->previous=&fiber->context;zend_fiber_transfertransfer=zend_fiber_resume(fiber,NULL,false);/*...*/}ZEND_METHOD(Fiber,suspend){/*...*/zend_fiber_transfertransfer=zend_fiber_suspend(fiber,value);/*...*/}ZEND_METHOD(Fiber,resume){/*...*/zend_fiber_transfertransfer=zend_fiber_resume(fiber,value,false);/*...*/}zend_fiber_resume和zend_fiber_suspend函数内部指向zend_fiber_switch_to函数(顾名思义,光纤切换)staticzend_always_inlinezend_fiber_transferzend_fiber_resume(zend_fiber*fiber,zval*value,boolexception){/*...*//*互相恢复=switchcontext到对方previous(启动时值为对方自己的,以后挂起和转移时可以替换)*/zend_fiber_transfertransfer=zend_fiber_switch_to(fiber->previous,value,exception);/*...*/}staticzend_always_inlinezend_fiber_transferzend_fiber_suspend(zend_fiber*fiber,zval*value){/*...*//*暂停自己=将上下文切换到调用你的那个*/returnzend_fiber_switch_to(caller,value,错误的);}zend_fiber_switch_to函数内部指向zend_fiber_switch_context函数(顾名思义就是切上下文),结合上面定义的Fiber上下文结构,最终应该是通过VM来切换CPU上的寄存器内容,所以这里就开始吧(再深一点,恐怕废话会露馅)/*...*/}还有一个函数staticzend_object初始化分配Fiber所需的空间*zend_fiber_object_create(zend_class_entry*ce){/*分配对象空间*/zend_fiber*fiber=emalloc(sizeof(zend_fiber));memset(纤维,0,sizeof(zend_fiber));/*...*/}阅读总结到了这里,可以知道猜测和实现略有不同,但基本相同总结和扩展从这个角度来看,Fiber(纤程)实际上是一个栈协程(用户态线程)的实现,所以它具有所有协程的特点;Fiber本身就是一个N:1的线程模型,也许可以通过结合多线程扩展来实现类似Golang的N:M模型(pthread已经废弃,还没试过,不过也需要结合第5点);Fiber将整个切换过程完全交给用户(开发者)控制,没有像Golang那样在runtime或engine/VM做任何控制/辅助调度功能;Fiber本身并不能解决IO阻塞问题。如果直接使用,不会提高效率,反而会带来额外的性能开销和读取成本;如果想有所作为的话,需要用Cli方式封装非阻塞IO和fiberscheduler(react和amp基本共享这个思路,但是基于generator+yield的方案),使用第二种方法构建N:M模型避免阻塞调用的影响,但这基本构成了一个简单的Golang调度运行时(参考《第9章Gogoroutine及其调度过程》);在FPM运行模式下,感觉有点没用,更像是一个低级的玩具API(可能8.1中官方加入Fiber只是第一步,以后可能会采取各种动作来配合随着性能和并发的提高,但也必然意味着复杂和变化~)。最后贴个Fiber和Swoole的瓜。好久没关注了,错过了剑光剑~
