歡迎來(lái)到「我是真的狗雜談世界」,關(guān)注不迷路
前言
很早就聽(tīng)說(shuō)PHP8.1出了Fiber(又稱纖程),但一直也沒(méi)時(shí)間搗鼓它,
正好前段時(shí)間在整理PHP的新特性/功能,想看看有沒(méi)有什么可以給日常開(kāi)發(fā)帶來(lái)便利、安全、性能提升的,再看到它感覺(jué)跟性能有點(diǎn)關(guān)系,
于是就決定搗鼓一下,整理記錄下?lián)v鼓過(guò)程。
使用
基本使用
作為開(kāi)發(fā)者(語(yǔ)言使用用戶),肯定先感受一下怎么用。按照官方的demo跑了一下:
$fiber = new Fiber(function (): void {
echo "我是第二個(gè)輸出\n";
Fiber::suspend();
echo "我是第四個(gè)輸出\n";
});
echo "我是第一個(gè)輸出\n";
$fiber->start();
echo "我是第三個(gè)輸出\n";
$fiber->resume();
echo "我是第五個(gè)輸出\n";
我是第一個(gè)輸出
我是第二個(gè)輸出
我是第三個(gè)輸出
我是第四個(gè)輸出
我是第五個(gè)輸出
看輸出順序不出所料,看demo的意思應(yīng)該還可以雙向傳遞數(shù)據(jù):
$fiber = new Fiber(function (string $fruit1, string $fruit2): void {
echo "我是第二個(gè)輸出;混合水果為:{$fruit1}, {$fruit2}\n";
$amount = Fiber::suspend("{$fruit1}{$fruit2}汁");
echo "我是第四個(gè)輸出;收銀為:{$amount}\n";
Fiber::suspend($amount - 23.5);
});
echo "我是第一個(gè)輸出\n";
$juice = $fiber->start("蘋(píng)果", "西瓜");
echo "我是第三個(gè)輸出;果汁為:{$juice}\n";
$change = $fiber->resume(50);
echo "我是第五個(gè)輸出;找零為:{$change}\n";
我是第一個(gè)輸出
我是第二個(gè)輸出;混合水果為:蘋(píng)果, 西瓜
我是第三個(gè)輸出;果汁為:蘋(píng)果西瓜汁
我是第四個(gè)輸出;收銀為:50
我是第五個(gè)輸出;找零為:26.5
對(duì)比生成器
好家伙,看起來(lái)是挺新鮮的,但是不是立馬想到了PHP5中就支持的生成器+yield(具體生成器+yield的使用說(shuō)明參考「Chapter 3.PHP5 生成器與yield及原理淺析」
)呢?
于是帶著疑問(wèn)閱讀了一下官方文檔,果然有寫(xiě)兩者的區(qū)別:

可以在無(wú)需改造中間函數(shù)模擬構(gòu)建調(diào)用堆棧的前提下,在任意位置進(jìn)行Fiber控制,而這在之前的生成器+yield的實(shí)現(xiàn)下是無(wú)法做到的
這也是之前PHP界一些協(xié)程框架一直被詬病之處,必須通過(guò)層層嵌套來(lái)模擬(模擬方式參考「Chapter 3.PHP5 生成器與yield及原理淺析」
嘗試一下(Fiber匿函中調(diào)用otherFunc時(shí)無(wú)需像yield那樣就行改造,就像正常函數(shù)調(diào)用一樣就可以):
$fiber = new Fiber(function (): void {
otherFunc();
});
echo "我要開(kāi)始咯\n";
$juice = $fiber->start();
echo "另一個(gè)函數(shù)成功暫停了Fiber;現(xiàn)在嘗試恢復(fù)Fiber\n";
$fiber->resume();
function otherFunc(): void
{
echo "Fiber已經(jīng)進(jìn)入了另一個(gè)函數(shù);現(xiàn)在在這里嘗試暫停該Fiber\n";
Fiber::suspend();
echo "Fiber已經(jīng)恢復(fù),我也結(jié)束自己的生命周期了\n";
}
我要開(kāi)始咯
Fiber已經(jīng)進(jìn)入了另一個(gè)函數(shù);現(xiàn)在在這里嘗試暫停該Fiber
另一個(gè)函數(shù)成功暫停了Fiber;現(xiàn)在嘗試恢復(fù)Fiber
Fiber已經(jīng)恢復(fù),我也結(jié)束自己的生命周期了
為什么?
知道了可以這樣用,總是想要再進(jìn)一步了解下為什么Fiber可以這么用,而生成器+yield卻不行呢?
帶著這個(gè)問(wèn)題開(kāi)始下一個(gè)環(huán)節(jié):
深入一點(diǎn)
繼續(xù)在官方文檔中尋找蛛絲馬跡,發(fā)現(xiàn)其實(shí)在同一段話中已經(jīng)把原因也說(shuō)清楚了~~

原因就是:
- 生成器則無(wú)棧
- Fiber具備獨(dú)立執(zhí)行堆棧(執(zhí)行堆棧相關(guān)知識(shí)參考「Chapter 4. 程序執(zhí)行過(guò)程中的執(zhí)行堆?!?/a>)
生成器執(zhí)行過(guò)程
生成器的執(zhí)行過(guò)程參考「Chapter 3.PHP5 生成器與yield及原理淺析」
Fiber執(zhí)行過(guò)程
基于Fiber具備獨(dú)立執(zhí)行堆棧的前提,不妨以下方代碼為例猜想一下Fiber大致執(zhí)行過(guò)程(先不管雙向值傳遞):
$fiber = new Fiber(function (): void {
otherFunc();
});
echo "我要開(kāi)始咯\n";
$juice = $fiber->start();
echo "另一個(gè)函數(shù)成功暫停了Fiber;現(xiàn)在嘗試恢復(fù)Fiber\n";
$fiber->resume();
function otherFunc(): void
{
echo "Fiber已經(jīng)進(jìn)入了另一個(gè)函數(shù);現(xiàn)在在這里嘗試暫停該Fiber\n";
Fiber::suspend();
echo "Fiber已經(jīng)恢復(fù),我也結(jié)束自己的生命周期了\n";
}
上下文信息初始化
- 分配一塊空間用于維護(hù)主執(zhí)行堆棧和全部Fiber執(zhí)行堆棧上下文信息的集合(圖中的101~200區(qū)塊)

圖中(下圖同)內(nèi)存只展示用戶空間幾個(gè)主要區(qū)塊,并且為了更簡(jiǎn)單展示Fiber的執(zhí)行過(guò)程,內(nèi)存、CPU以及ZendVM屏蔽和抽象了一些細(xì)節(jié),如需更多了解細(xì)節(jié),可參考以下文章:
Fiber的初始化
$fiber = new Fiber(function (): void {
otherFunc();
});
當(dāng)CPU以主線程棧區(qū)為執(zhí)行堆棧執(zhí)行到002行時(shí):
- 在用戶空間分配一塊內(nèi)存(一般從堆中)充當(dāng)該Fiber的執(zhí)行堆棧(圖中9900~9801區(qū)塊)
- 將該Fiber狀態(tài)以及其執(zhí)行堆棧上下文信息加入上下文維護(hù)集合(圖中106~110,可以認(rèn)為Fiber包的指令起始位置為003行,因此EIP為003;匿函沒(méi)有參數(shù)和局部變量,因此ESP和EBP都指向9900)

Fiber的啟動(dòng)
$juice = $fiber->start();
CPU繼續(xù)以主線程棧區(qū)為執(zhí)行堆棧執(zhí)行了005行,執(zhí)行到006行時(shí):
- 暫存當(dāng)前CPU上下文至維護(hù)集合中主執(zhí)行堆棧上下文信息中(圖中101~105,當(dāng)前執(zhí)行指令行為006,因此EIP為007)
- 從維護(hù)集合中將目標(biāo)Fiber標(biāo)記為激活狀態(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)用的過(guò)程參考「Chapter 4. 程序執(zhí)行過(guò)程中的執(zhí)行堆?!?/a>

Fiber的暫停
Fiber::suspend();
CPU以Fiber棧區(qū)為執(zhí)行堆棧執(zhí)行012行,執(zhí)行到013行時(shí):
- 暫存當(dāng)前CPU上下文至維護(hù)集合中代表該Fiber的上下文信息中(圖中106~110,當(dāng)前執(zhí)行指令行為013,因此EIP為014;當(dāng)前Fiber在otherFunc中且無(wú)參數(shù)和局部變量,因此ESP和EBP指向otherFunc棧幀,也就是9899)
- 將該Fiber標(biāo)記為休眠狀態(tài)
- 從維護(hù)集合中將主執(zhí)行堆棧上下文信息(圖中101~105)填充至當(dāng)前CPU上下文中

Fiber的恢復(fù)
$fiber->resume();
CPU以主線程棧區(qū)為執(zhí)行堆棧執(zhí)行007行,執(zhí)行到008行時(shí)(同上述 Fiber的啟動(dòng)過(guò)程):
- 暫存當(dāng)前CPU上下文至維護(hù)集合中主執(zhí)行堆棧上下文信息中(圖中101~105,當(dāng)前執(zhí)行指令行為008,因此EIP為009)
- 從維護(hù)集合中將目標(biāo)Fiber標(biāo)記為激活狀態(tài),并將其執(zhí)行堆棧上下文信息(圖中106~110)填充至當(dāng)前CPU上下文中

Fiber的終結(jié)
CPU以Fiber棧區(qū)為執(zhí)行堆棧執(zhí)行014行,otherFuc函數(shù)返回(棧幀出棧),回到004行,F(xiàn)iber終結(jié)時(shí):
- 從維護(hù)集合中刪除目標(biāo)Fiber的狀態(tài)信息及其執(zhí)行堆棧上下文信息(圖中106~110)
- 銷毀該Fiber對(duì)應(yīng)的執(zhí)行堆棧(圖中9801~9900)
- 將主線程(啟動(dòng)/喚醒該Fiber的調(diào)用者)上下文信息(圖中101~105)填充至當(dāng)前CPU上下文中,使CPU回到主線程棧運(yùn)行

源碼驗(yàn)證
以上過(guò)程只是猜測(cè),為了驗(yàn)證這個(gè)猜測(cè)的正確性與正確度,帶著三腳貓的C記憶來(lái)看一下Fiber的源碼實(shí)現(xiàn):
核心代碼文件
先找到幾個(gè)主要文件中的主要代碼塊(還好PHP源碼很容易就能找到這倆文件):
Fiber核心結(jié)構(gòu)
為Fiber設(shè)置了初始化、運(yùn)行中、暫停、死亡四種狀態(tài):
typedef enum {
ZEND_FIBER_STATUS_INIT,
ZEND_FIBER_STATUS_RUNNING,
ZEND_FIBER_STATUS_SUSPENDED,
ZEND_FIBER_STATUS_DEAD,
} zend_fiber_status;
每個(gè)Fiber的上下文結(jié)構(gòu)中維護(hù)了該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];
};
一個(gè)Fiber結(jié)構(gòu)中包含了自身、調(diào)用者、恢復(fù)目標(biāo)的上下文,執(zhí)行堆棧當(dāng)前棧底幀??雌饋?lái)不是獨(dú)立一塊空間維護(hù)全部上下文,而是自維護(hù)并組成鏈表
/* */
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ù)(顧名思義進(jìn)行fiber切換)
static zend_always_inline zend_fiber_transfer zend_fiber_resume(zend_fiber *fiber, zval *value, bool exception)
{
/* ... */
/* 恢復(fù)對(duì)方=切換上下文至對(duì)方的previous(start時(shí)該值為對(duì)方自身,之后可在暫停和傳遞時(shí)被替換) */
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)用自己的那個(gè) */
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)該就是通過(guò)VM切換CPU上的寄存器內(nèi)容了,就到這兒吧(再深入怕胡說(shuō)八道露了餡)
static zend_always_inline zend_fiber_transfer zend_fiber_switch_to(
zend_fiber_context *context, zval *value, bool exception
) {
/* ... */
zend_fiber_switch_context(&transfer);
/* ... */
}
- 還有一個(gè)初始化分配Fiber所需空間的函數(shù)
static zend_object *zend_fiber_object_create(zend_class_entry *ce)
{
/* 分配對(duì)象空間 */
zend_fiber *fiber = emalloc(sizeof(zend_fiber));
memset(fiber, 0, sizeof(zend_fiber));
/* ... */
}
閱讀小結(jié)
到了這里,可以知道猜測(cè)跟實(shí)現(xiàn)略有出入,但基本差不多。
總結(jié)與擴(kuò)展
- 這樣看起來(lái),F(xiàn)iber(纖程)其實(shí)就是一種有棧協(xié)程(用戶態(tài)線程)的實(shí)現(xiàn),因此它具備全部協(xié)程的特點(diǎn);
- Fiber本身是一種N:1的線程模型,也許可以結(jié)合多線程擴(kuò)展來(lái)實(shí)現(xiàn)類似Golang的N:M模型(pthread被放棄了,還未嘗試,不過(guò)也需要結(jié)合第5點(diǎn));
- Fiber將全部的切換過(guò)程完全交由用戶(開(kāi)發(fā)者)控制,沒(méi)有像Golang在runtime或者說(shuō)引擎/VM中做任何掌控/協(xié)助調(diào)度的功能;
- Fiber本身并沒(méi)有解決IO阻塞問(wèn)題,如果直接用它不會(huì)提升效率,反而會(huì)帶來(lái)額外的性能開(kāi)銷和閱讀成本;
- 如果想要有所發(fā)揮,感覺(jué)需要在Cli運(yùn)行模式下封裝非阻塞IO和fiber調(diào)度器(react和amp基本都是這思路,只是基于生成器+yield方案),同時(shí)用第2點(diǎn)的方式構(gòu)建N:M模型來(lái)避免阻塞調(diào)用的影響,但這樣基本都構(gòu)成一套簡(jiǎn)易Golang的調(diào)度runtime(參考「Chapter 9. Go goroutine與其調(diào)度過(guò)程」);
- 而在FPM運(yùn)行模式下,感覺(jué)有點(diǎn)沒(méi)什么用武之地,更像是一個(gè)底層的玩具API(或許官方在8.1加入Fiber只是第一步,未來(lái)可能會(huì)從各方面動(dòng)作來(lái)配合實(shí)現(xiàn)性能和并發(fā)的提升,但勢(shì)必也意味著復(fù)雜和變化吧~)。
最后貼一個(gè)Fiber和Swoole的瓜,許久沒(méi)關(guān)注,錯(cuò)過(guò)了刀光劍影~