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

歡迎來到「我是真的狗雜談世界」,關(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ū)別:

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

可以在無需改造中間函數(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)把原因也說清楚了~~

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

原因就是:

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

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

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上下文中
img5-Fiber啟動.png

Fiber中調(diào)用otherFunc函數(shù)

CPU以Fiber棧區(qū)為執(zhí)行堆棧執(zhí)行003行,調(diào)用otherFunc函數(shù)邑时,函數(shù)調(diào)用的過程參考「Chapter 4. 程序執(zhí)行過程中的執(zhí)行堆椗簦」

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

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

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

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回到主線程棧運行
img9-Fiber終結(jié).png

源碼驗證

以上過程只是猜測痊末,為了驗證這個猜測的正確性與正確度,帶著三腳貓的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_resumezend_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é)與擴展

  1. 這樣看起來卷员,F(xiàn)iber(纖程)其實就是一種有棧協(xié)程(用戶態(tài)線程)的實現(xiàn)盈匾,因此它具備全部協(xié)程的特點;
  2. Fiber本身是一種N:1的線程模型毕骡,也許可以結(jié)合多線程擴展來實現(xiàn)類似Golang的N:M模型(pthread被放棄了削饵,還未嘗試,不過也需要結(jié)合第5點)未巫;
  3. Fiber將全部的切換過程完全交由用戶(開發(fā)者)控制窿撬,沒有像Golang在runtime或者說引擎/VM中做任何掌控/協(xié)助調(diào)度的功能;
  4. Fiber本身并沒有解決IO阻塞問題叙凡,如果直接用它不會提升效率劈伴,反而會帶來額外的性能開銷和閱讀成本;
  5. 如果想要有所發(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)度過程」)追城;
  6. 而在FPM運行模式下,感覺有點沒什么用武之地燥撞,更像是一個底層的玩具API(或許官方在8.1加入Fiber只是第一步座柱,未來可能會從各方面動作來配合實現(xiàn)性能和并發(fā)的提升,但勢必也意味著復(fù)雜和變化吧~)物舒。

最后貼一個Fiber和Swoole的瓜色洞,許久沒關(guān)注,錯過了刀光劍影~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末冠胯,一起剝皮案震驚了整個濱河市锋玲,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌涵叮,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伞插,死亡現(xiàn)場離奇詭異割粮,居然都是意外死亡,警方通過查閱死者的電腦和手機媚污,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門舀瓢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人耗美,你說我怎么就攤上這事京髓『阶海” “怎么了?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵堰怨,是天一觀的道長芥玉。 經(jīng)常有香客問我,道長备图,這世上最難降的妖魔是什么灿巧? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮揽涮,結(jié)果婚禮上抠藕,老公的妹妹穿的比我還像新娘。我一直安慰自己蒋困,他們只是感情好盾似,可當(dāng)我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著雪标,像睡著了一般零院。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上汰聋,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天门粪,我揣著相機與錄音,去河邊找鬼烹困。 笑死玄妈,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的髓梅。 我是一名探鬼主播拟蜻,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼枯饿!你這毒婦竟也來了酝锅?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤奢方,失蹤者是張志新(化名)和其女友劉穎搔扁,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蟋字,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡稿蹲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了鹊奖。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片苛聘。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出设哗,到底是詐尸還是另有隱情唱捣,我是刑警寧澤,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布网梢,位于F島的核電站震缭,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏澎粟。R本人自食惡果不足惜蛀序,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望活烙。 院中可真熱鬧徐裸,春花似錦、人聲如沸啸盏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽回懦。三九已至气笙,卻和暖如春猖腕,著一層夾襖步出監(jiān)牢的瞬間迅耘,已是汗流浹背溃论。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工象颖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人馁蒂。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓饱亮,卻偏偏與公主長得像兆旬,于是被迫代替她去往敵國和親吧凉。 傳聞我的和親對象是個殘疾皇子隧出,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容