歡迎來到「我是真的狗雜談世界」,關(guān)注不迷路
前言
很早就聽說PHP8.1出了Fiber(又稱纖程)泰演,但一直也沒時間搗鼓它,
正好前段時間在整理PHP的新特性/功能,想看看有沒有什么可以給日常開發(fā)帶來便利瓮下、安全搔确、性能提升的,再看到它感覺跟性能有點關(guān)系荠商,
于是就決定搗鼓一下皆撩,整理記錄下?lián)v鼓過程扣墩。
使用
基本使用
作為開發(fā)者(語言使用用戶),肯定先感受一下怎么用毅访。按照官方的demo跑了一下:
$fiber = new Fiber(function (): void {
echo "我是第二個輸出\n";
Fiber::suspend();
echo "我是第四個輸出\n";
});
echo "我是第一個輸出\n";
$fiber->start();
echo "我是第三個輸出\n";
$fiber->resume();
echo "我是第五個輸出\n";
我是第一個輸出
我是第二個輸出
我是第三個輸出
我是第四個輸出
我是第五個輸出
看輸出順序不出所料沮榜,看demo的意思應(yīng)該還可以雙向傳遞數(shù)據(jù):
$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及原理淺析」
)呢?
于是帶著疑問閱讀了一下官方文檔由境,果然有寫兩者的區(qū)別:
可以在無需改造中間函數(shù)模擬構(gòu)建調(diào)用堆棧的前提下棚亩,在任意位置進行Fiber控制,而這在之前的生成器+yield的實現(xiàn)下是無法做到的
這也是之前PHP界一些協(xié)程框架一直被詬病之處虏杰,必須通過層層嵌套來模擬(模擬方式參考「Chapter 3.PHP5 生成器與yield及原理淺析」
嘗試一下(Fiber匿函中調(diào)用otherFunc時無需像yield那樣就行改造讥蟆,就像正常函數(shù)調(diào)用一樣就可以):
$fiber = new Fiber(function (): void {
otherFunc();
});
echo "我要開始咯\n";
$juice = $fiber->start();
echo "另一個函數(shù)成功暫停了Fiber;現(xiàn)在嘗試恢復(fù)Fiber\n";
$fiber->resume();
function otherFunc(): void
{
echo "Fiber已經(jīng)進入了另一個函數(shù)纺阔;現(xiàn)在在這里嘗試暫停該Fiber\n";
Fiber::suspend();
echo "Fiber已經(jīng)恢復(fù)瘸彤,我也結(jié)束自己的生命周期了\n";
}
我要開始咯
Fiber已經(jīng)進入了另一個函數(shù);現(xiàn)在在這里嘗試暫停該Fiber
另一個函數(shù)成功暫停了Fiber笛钝;現(xiàn)在嘗試恢復(fù)Fiber
Fiber已經(jīng)恢復(fù)质况,我也結(jié)束自己的生命周期了
為什么?
知道了可以這樣用玻靡,總是想要再進一步了解下為什么Fiber可以這么用结榄,而生成器+yield卻不行呢?
帶著這個問題開始下一個環(huán)節(jié):
深入一點
繼續(xù)在官方文檔中尋找蛛絲馬跡囤捻,發(fā)現(xiàn)其實在同一段話中已經(jīng)把原因也說清楚了~~
原因就是:
- 生成器則無棧
- Fiber具備獨立執(zhí)行堆棧(執(zhí)行堆棧相關(guān)知識參考「Chapter 4. 程序執(zhí)行過程中的執(zhí)行堆椞杜悖」)
生成器執(zhí)行過程
生成器的執(zhí)行過程參考「Chapter 3.PHP5 生成器與yield及原理淺析」
Fiber執(zhí)行過程
基于Fiber具備獨立執(zhí)行堆棧的前提,不妨以下方代碼為例猜想一下Fiber大致執(zhí)行過程(先不管雙向值傳遞):
$fiber = new Fiber(function (): void {
otherFunc();
});
echo "我要開始咯\n";
$juice = $fiber->start();
echo "另一個函數(shù)成功暫停了Fiber最蕾;現(xiàn)在嘗試恢復(fù)Fiber\n";
$fiber->resume();
function otherFunc(): void
{
echo "Fiber已經(jīng)進入了另一個函數(shù);現(xiàn)在在這里嘗試暫停該Fiber\n";
Fiber::suspend();
echo "Fiber已經(jīng)恢復(fù)老厌,我也結(jié)束自己的生命周期了\n";
}
上下文信息初始化
- 分配一塊空間用于維護主執(zhí)行堆棧和全部Fiber執(zhí)行堆棧上下文信息的集合(圖中的101~200區(qū)塊)
圖中(下圖同)內(nèi)存只展示用戶空間幾個主要區(qū)塊瘟则,并且為了更簡單展示Fiber的執(zhí)行過程,內(nèi)存枝秤、CPU以及ZendVM屏蔽和抽象了一些細節(jié)醋拧,如需更多了解細節(jié),可參考以下文章:
Fiber的初始化
$fiber = new Fiber(function (): void {
otherFunc();
});
當(dāng)CPU以主線程棧區(qū)為執(zhí)行堆棧執(zhí)行到002行時:
- 在用戶空間分配一塊內(nèi)存(一般從堆中)充當(dāng)該Fiber的執(zhí)行堆棧(圖中9900~9801區(qū)塊)
- 將該Fiber狀態(tài)以及其執(zhí)行堆棧上下文信息加入上下文維護集合(圖中106~110丹壕,可以認為Fiber包的指令起始位置為003行,因此EIP為003薇溃;匿函沒有參數(shù)和局部變量菌赖,因此ESP和EBP都指向9900)
Fiber的啟動
$juice = $fiber->start();
CPU繼續(xù)以主線程棧區(qū)為執(zhí)行堆棧執(zhí)行了005行,執(zhí)行到006行時:
- 暫存當(dāng)前CPU上下文至維護集合中主執(zhí)行堆棧上下文信息中(圖中101~105沐序,當(dāng)前執(zhí)行指令行為006琉用,因此EIP為007)
- 從維護集合中將目標Fiber標記為激活狀態(tài)堕绩,并將其執(zhí)行堆棧上下文信息(圖中106~110)填充至當(dāng)前CPU上下文中
Fiber中調(diào)用otherFunc函數(shù)
CPU以Fiber棧區(qū)為執(zhí)行堆棧執(zhí)行003行,調(diào)用otherFunc函數(shù)邑时,函數(shù)調(diào)用的過程參考「Chapter 4. 程序執(zhí)行過程中的執(zhí)行堆椗簦」
Fiber的暫停
Fiber::suspend();
CPU以Fiber棧區(qū)為執(zhí)行堆棧執(zhí)行012行,執(zhí)行到013行時:
- 暫存當(dāng)前CPU上下文至維護集合中代表該Fiber的上下文信息中(圖中106~110晶丘,當(dāng)前執(zhí)行指令行為013黍氮,因此EIP為014;當(dāng)前Fiber在otherFunc中且無參數(shù)和局部變量浅浮,因此ESP和EBP指向otherFunc棧幀沫浆,也就是9899)
- 將該Fiber標記為休眠狀態(tài)
- 從維護集合中將主執(zhí)行堆棧上下文信息(圖中101~105)填充至當(dāng)前CPU上下文中
Fiber的恢復(fù)
$fiber->resume();
CPU以主線程棧區(qū)為執(zhí)行堆棧執(zhí)行007行,執(zhí)行到008行時(同上述 Fiber的啟動過程):
- 暫存當(dāng)前CPU上下文至維護集合中主執(zhí)行堆棧上下文信息中(圖中101~105脑题,當(dāng)前執(zhí)行指令行為008件缸,因此EIP為009)
- 從維護集合中將目標Fiber標記為激活狀態(tài),并將其執(zhí)行堆棧上下文信息(圖中106~110)填充至當(dāng)前CPU上下文中
Fiber的終結(jié)
CPU以Fiber棧區(qū)為執(zhí)行堆棧執(zhí)行014行叔遂,otherFuc函數(shù)返回(棧幀出棧)他炊,回到004行,F(xiàn)iber終結(jié)時:
- 從維護集合中刪除目標Fiber的狀態(tài)信息及其執(zhí)行堆棧上下文信息(圖中106~110)
- 銷毀該Fiber對應(yīng)的執(zhí)行堆棧(圖中9801~9900)
- 將主線程(啟動/喚醒該Fiber的調(diào)用者)上下文信息(圖中101~105)填充至當(dāng)前CPU上下文中已艰,使CPU回到主線程棧運行
源碼驗證
以上過程只是猜測痊末,為了驗證這個猜測的正確性與正確度,帶著三腳貓的C記憶來看一下Fiber的源碼實現(xiàn):
核心代碼文件
先找到幾個主要文件中的主要代碼塊(還好PHP源碼很容易就能找到這倆文件):
Fiber核心結(jié)構(gòu)
為Fiber設(shè)置了初始化哩掺、運行中凿叠、暫停、死亡四種狀態(tài):
typedef enum {
ZEND_FIBER_STATUS_INIT,
ZEND_FIBER_STATUS_RUNNING,
ZEND_FIBER_STATUS_SUSPENDED,
ZEND_FIBER_STATUS_DEAD,
} zend_fiber_status;
每個Fiber的上下文結(jié)構(gòu)中維護了該Fiber的函數(shù)入口嚼吞、執(zhí)行堆棧盒件、狀態(tài)等信息
struct _zend_fiber_context {
/* Pointer to boost.context or ucontext_t data. */
void *handle;
/* Pointer that identifies the fiber type. */
void *kind;
/* Entrypoint function of the fiber. */
zend_fiber_coroutine function;
/* Cleanup function for fiber. */
zend_fiber_clean cleanup;
/* Assigned C stack. */
zend_fiber_stack *stack;
/* Fiber status. */
zend_fiber_status status;
/* Observer state */
zend_execute_data *top_observed_frame;
/* Reserved for extensions */
void *reserved[ZEND_MAX_RESERVED_RESOURCES];
};
一個Fiber結(jié)構(gòu)中包含了自身、調(diào)用者舱禽、恢復(fù)目標的上下文炒刁,執(zhí)行堆棧當(dāng)前棧底幀√苤桑看起來不是獨立一塊空間維護全部上下文翔始,而是自維護并組成鏈表
/* */
struct _zend_fiber {
/* PHP object handle. */
zend_object std;
/* Flags are defined in enum zend_fiber_flag. */
uint8_t flags;
/* Native C fiber context. */
zend_fiber_context context;
/* Fiber that resumed us. */
zend_fiber_context *caller;
/* Fiber that suspended us. */
zend_fiber_context *previous;
/* Callback and info / cache to be used when fiber is started. */
zend_fcall_info fci;
zend_fcall_info_cache fci_cache;
/* Current Zend VM execute data being run by the fiber. */
zend_execute_data *execute_data;
/* Frame on the bottom of the fiber vm stack. */
zend_execute_data *stack_bottom;
/* Active fiber vm stack. */
zend_vm_stack vm_stack;
/* Storage for fiber return value. */
zval result;
};
Fiber核心功能
為了方便閱讀,源碼中僅留下重要代碼里伯,前后用/* ... */代替了:
-
Fiber::start
城瞎、Fiber::resume
方法內(nèi)部指向zend_fiber_resume
函數(shù) -
Fiber::suspend
方法內(nèi)部指向zend_fiber_suspend
函數(shù)
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
函數(shù)內(nèi)部指向zend_fiber_switch_to
函數(shù)(顧名思義進行fiber切換)
static zend_always_inline zend_fiber_transfer zend_fiber_resume(zend_fiber *fiber, zval *value, bool exception)
{
/* ... */
/* 恢復(fù)對方=切換上下文至對方的previous(start時該值為對方自身疾瓮,之后可在暫停和傳遞時被替換) */
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)
{
/* ... */
/* 暫停自己=切換上下文至調(diào)用自己的那個 */
return zend_fiber_switch_to(caller, value, false);
}
-
zend_fiber_switch_to
函數(shù)內(nèi)部指向zend_fiber_switch_context
函數(shù)(顧名思義切上下文)脖镀,結(jié)合上面定義的Fiber上下文結(jié)構(gòu),最終應(yīng)該就是通過VM切換CPU上的寄存器內(nèi)容了狼电,就到這兒吧(再深入怕胡說八道露了餡)
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所需空間的函數(shù)
static zend_object *zend_fiber_object_create(zend_class_entry *ce)
{
/* 分配對象空間 */
zend_fiber *fiber = emalloc(sizeof(zend_fiber));
memset(fiber, 0, sizeof(zend_fiber));
/* ... */
}
閱讀小結(jié)
到了這里认然,可以知道猜測跟實現(xiàn)略有出入补憾,但基本差不多。
總結(jié)與擴展
- 這樣看起來卷员,F(xiàn)iber(纖程)其實就是一種有棧協(xié)程(用戶態(tài)線程)的實現(xiàn)盈匾,因此它具備全部協(xié)程的特點;
- Fiber本身是一種N:1的線程模型毕骡,也許可以結(jié)合多線程擴展來實現(xiàn)類似Golang的N:M模型(pthread被放棄了削饵,還未嘗試,不過也需要結(jié)合第5點)未巫;
- Fiber將全部的切換過程完全交由用戶(開發(fā)者)控制窿撬,沒有像Golang在runtime或者說引擎/VM中做任何掌控/協(xié)助調(diào)度的功能;
- Fiber本身并沒有解決IO阻塞問題叙凡,如果直接用它不會提升效率劈伴,反而會帶來額外的性能開銷和閱讀成本;
- 如果想要有所發(fā)揮握爷,感覺需要在Cli運行模式下封裝非阻塞IO和fiber調(diào)度器(react和amp基本都是這思路跛璧,只是基于生成器+yield方案),同時用第2點的方式構(gòu)建N:M模型來避免阻塞調(diào)用的影響新啼,但這樣基本都構(gòu)成一套簡易Golang的調(diào)度runtime(參考「Chapter 9. Go goroutine與其調(diào)度過程」)追城;
- 而在FPM運行模式下,感覺有點沒什么用武之地燥撞,更像是一個底層的玩具API(或許官方在8.1加入Fiber只是第一步座柱,未來可能會從各方面動作來配合實現(xiàn)性能和并發(fā)的提升,但勢必也意味著復(fù)雜和變化吧~)物舒。
最后貼一個Fiber和Swoole的瓜色洞,許久沒關(guān)注,錯過了刀光劍影~