是該努力點了!
1210 字
6 分鐘
PHP8.1-Fiber
2024-04-10

PHP8.1 - Fiber#

現在的程式語言競爭激烈,Go 有 Goroutine, Java 有 Coroutine, Nodejs 有 async/await 異步; 我在看這些東西的時候,就會在想好多程式語言的特性,是不是現在都漸漸的在想辦法把一個進程下想辦法自己再開了一個小攜程, 使應用程式自己控制自己的調度行為,而非使用 thread, multi process 去使用作業系統的調度。(減少開銷)

那在 PHP 8.1 中,新增了一個 class - Fiber,這個 class 讓 PHP 可以將任務切分的更小塊; 達成一些功能上的短暫切換,讓開發者可以更加靈活自己控制自己的應用程式。

Fiber 的一些方法 (查看 參考的#2)#

  • Fiber::__construct: start 會第一個呼叫
  • Fiber::start: 啟動 Fiber,會呼叫 __construct 的 Closure $callback
  • Fiber::suspend: 暫停 Fiber
  • Fiber::resume: 回復 Fiber
  • Fiber::getCurrent: 取得目前 Fiber 的實例
  • Fiber::getReturn: 取得 Fiber 回傳: Fiber 有 return 結果的話才能取得結果,並且在最後一次暫停時也要整個完整結束,他才會正常取得最後的返回值
  • Fiber::throw: 傳入異常可使 Fiber 拋例外
  • Fiber::isStarted: 確認 Fiber 是否已經啟動了
  • Fiber::isSuspended: 是否暫停 Fiber
  • Fiber::isRunning: Fiber 是否正在 running
  • Fiber::isTerminated: Fiber 是否中止

例子1. 任務間的暫停與切換#

定義了 SuspendData 的 class、Status 是一個 enum(Stop, Running),宣告兩個 Fiber 推入陣列中, 但這些只是宣告,並沒有被觸發執行,兩個纖程到最下面 while(count($reg)) 的時候,才被輪流被執行。

class SuspendData
{
    public function __construct(
        public readonly Status $status
    ) {}
}

enum Status
{
    case Stop;
    case Running;
}

/** @var array<Fiber> $reg */
$reg = []; // 註冊一些任務集合進入 array -> array<fiber>
$startFlag = true;

/**
 * A, B 這兩個 Fiber 任務推入 $reg 這個 array 中
 * ----------------------------------------------------
 * A: 每次執行到 i % 3 === 0 的時候,就會暫停,直到被 resume
 * B: 每次執行都會暫停,直到被 resume
 */
// Fiber - A (推入$reg)
$reg[] = new Fiber(function () {
    for ($i = 0; $i < 10; $i++) {
        echo "[A] Fiber: $i\n";
        if ($i % 3 === 0) {
            Fiber::suspend(new SuspendData(Status::Running));
        }
    }

    echo "[A] Fiber: Done\n";
    Fiber::suspend(new SuspendData(Status::Stop));
});

// Fiber - B (推入$reg)
$reg[] = new Fiber(function () {
    for ($i = 0; $i < 10; $i++) {
        echo "[B] Fiber: $i\n";
        Fiber::suspend(new SuspendData(Status::Running));
    }

    echo "[B] Fiber: Done\n";
    Fiber::suspend(new SuspendData(Status::Stop));
});


/**
 * 如果 $reg 這個 array 還有任務,就繼續執行
 * 當 Fiber 的任務完成後,就把該任務從 $reg 中移除
 */
while (count($reg) > 0) {
    if ($startFlag) foreach ($reg as $k => $fiber) {
        $fiber->start();
        $startFlag = false;
    }
    foreach ($reg as $k => $fiber) {
        $r = $fiber->resume();

        if ($r->status === Status::Stop) {
            unset($reg[$k]);
        }
    }
}

例子2. 雙向傳遞資料#

藉由暫停纖程,傳入傳出所需要參數,並且進行交互的行為。

$fb = new Fiber(function (string $fruit1, string $fruit2): void {

    echo "選定的水果是: {$fruit1} 和 {$fruit2}\n";
    $amount = Fiber::suspend("{$fruit1}+{$fruit2}汁"); // 第一次暫停,並且回傳果汁
    echo "總共 {$amount} 元\n";
    $paid = Fiber::suspend("{$fruit1}+{$fruit2}汁,{$amount}"); // 第二次暫停,並且回傳
    
    echo "您付了 {$paid} 元\n";
    if ($paid < $amount) {
        echo "您付的錢不夠,請再付一點錢\n";
        Fiber::suspend(0);
    }

    echo "找您 " . ($paid - $amount) . "元\n";
    echo "謝謝惠顧\n";
    Fiber::suspend(1);
});


$result = $fb->start("蘋果", "香蕉"); // 啟動 Fiber 並傳入參數
print_r($result . PHP_EOL); // 輸出結果
$clerkSay = $fb->resume(30); // 恢復 Fiber 並傳入參數
print_r('店員: '. $clerkSay . '元' . PHP_EOL); // 輸出結果
$finished = $fb->resume(100); // >>>> 調整24, 25 行註解可以看到不同結果
// $finished = $fb->resume(20); // >>>> 調整24, 25 行註解可以看到不同結果

if ($finished) {
    echo "交易完成\n";
} else {
    echo "交易失敗\n";
}

例子3. 使用 callback 暫停#

透過外部 callback 暫停正在執行的纖程。

$continueFiberCallback = function (string $str) {
    Fiber::suspend($str);
};

$fb = new Fiber(function () use ($continueFiberCallback) {
    $continueFiberCallback("[1] Hello world");
    $continueFiberCallback("[2] Hello PHP");
    $continueFiberCallback("[3] Hello Go");
    $continueFiberCallback("[4] Hello Rust");
});

$r = $fb->start();
echo "Fiber started with $r\n";
$result[] = $fb->resume();

for ($i = 0; $i < 2; $i++) {
    $result[] = $fb->resume();
}

print_r($result);
echo "Done\n";

後話#

Fiber 給我的感覺有點像是 goto 跳躍一樣,使得程式可以在自己想要的地方跳躍過去,跳躍回來(暫停先前的操作)。 而我目前無法感受出這些在哪裡會用到… Fiber 感覺會使程式跳來跳去的。 目前只有想到 I/O 或是 Network 這種可以做,但是不清楚要怎麼實現(囧

後來看到關於友人討論,找到關於 參考#3, #4 的文章,在 #3 最下面談論到的第四點,本身 Fiber 沒有解決 I/O 的問題,如果直接使用不會有提升效能,反而帶來額外的開銷,#4 中也提到,PHP 加入 Fiber 蠻倉促的,並且 PHP 中,協程(攜程)的支援有點晚(?) 所以要用 Fiber 的話,要斟酌一下使用場景。

參考#