Chapter 2.PHP8.1 新特性fiber及原理淺析

歡迎來(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ū)別:

img1-Fiber與生成器使用區(qū)別.jpg

可以在無(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ō)清楚了~~

img2-Fiber與生成器實(shí)現(xiàn)上區(qū)別.jpg

原因就是:

生成器執(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ū)塊)
img3-上下文信息堆初始化.png

圖中(下圖同)內(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)
img4-Fiber初始化.png

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上下文中
img5-Fiber啟動(dòng).png

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>

img6-Fiber調(diào)用otherFunc.png

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上下文中
img7-Fiber暫停.png

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上下文中
img8-Fiber恢復(fù).png

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)行
img9-Fiber終結(jié).png

源碼驗(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_resumezend_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ò)展

  1. 這樣看起來(lái),F(xiàn)iber(纖程)其實(shí)就是一種有棧協(xié)程(用戶態(tài)線程)的實(shí)現(xiàn),因此它具備全部協(xié)程的特點(diǎn);
  2. Fiber本身是一種N:1的線程模型,也許可以結(jié)合多線程擴(kuò)展來(lái)實(shí)現(xiàn)類似Golang的N:M模型(pthread被放棄了,還未嘗試,不過(guò)也需要結(jié)合第5點(diǎn));
  3. Fiber將全部的切換過(guò)程完全交由用戶(開(kāi)發(fā)者)控制,沒(méi)有像Golang在runtime或者說(shuō)引擎/VM中做任何掌控/協(xié)助調(diào)度的功能;
  4. Fiber本身并沒(méi)有解決IO阻塞問(wèn)題,如果直接用它不會(huì)提升效率,反而會(huì)帶來(lái)額外的性能開(kāi)銷和閱讀成本;
  5. 如果想要有所發(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ò)程」);
  6. 而在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ò)了刀光劍影~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容