PHP Conference Japan 2024

Fiber

Fiber 概述

(PHP 8 >= 8.1.0)

Fiber 代表完整的、可中斷的函式。Fiber 可以從呼叫堆疊中的任何位置暫停,在 Fiber 內部暫停執行,直到稍後恢復 Fiber 為止。

Fiber 會暫停整個執行堆疊,因此函式的直接呼叫者不需要更改其呼叫函式的方式。

可以使用 Fiber::suspend() 中斷呼叫堆疊中任何位置的執行(也就是說,對 Fiber::suspend() 的呼叫可能位於深度巢狀函式中,甚至根本不存在)。

與無堆疊的產生器 (Generator)不同,每個纖程 (Fiber)都有自己的呼叫堆疊,允許它們在深度巢狀的函數呼叫中暫停。宣告中斷點的函數(即呼叫 Fiber::suspend())不需要更改其返回類型,這與使用 yield 的函數不同,後者必須返回一個 產生器 (Generator) 實例。

纖程可以在任何函數呼叫中暫停,包括從 PHP VM 內部呼叫的函數,例如提供給 array_map() 的函數或由 foreach迭代器 (Iterator) 物件上呼叫的方法。

一旦暫停,可以使用 Fiber::resume() 以任何值恢復纖程的執行,或者使用 Fiber::throw() 將異常拋入纖程。該值會從 Fiber::suspend() 返回(或拋出異常)。

注意 在 PHP 8.4.0 之前,不允許在物件 解構器 (destructor) 執行期間切換纖程。

範例 #1 基本用法

<?php
$fiber
= new Fiber(function (): void {
$value
= Fiber::suspend('fiber');
echo
"用於恢復纖程的值:", $value, PHP_EOL;
});

$value
= $fiber->start();

echo
"纖程暫停時的值:", $value, PHP_EOL;

$fiber->
resume('test');
?>

以上範例將輸出:

Value from fiber suspending: fiber
Value used to resume fiber: test
新增註釋

使用者貢獻的註釋 6 則註釋

user at csa dot es
2 年前
也許不要在所有地方都使用相同的變數名稱會是個好主意。

<?php
$fiber
= new Fiber(function (): void {
$parm = Fiber::suspend('fiber');
echo
"用於恢復 fiber 的值:", $parm, PHP_EOL;
});

$res = $fiber->start();

echo
"Fiber 掛起時的值:", $res, PHP_EOL;

$fiber->resume('test');
?>
Ali Madadi
2 年前
這是一個簡單的排程器和線程池,它使用 PHP 8.1 中的 Fiber 和 tick 函數實現多線程,並在最後以陣列形式返回池中每個函數的返回值。

請注意,由於一些錯誤,您需要為每個「線程」註冊一個新的 tick 函數。記住在最後取消註冊所有 tick 函數。

以下連結是目前(撰寫本文時)正在進行的錯誤討論。請注意,根據討論,在 PHP 8.2+ 中,在 tick 函數內調用 Fiber::suspend() 的功能可能會被禁止。但如果錯誤得到修復,您可以將 register_tick_function() 行移動到類的頂部,這個用純 PHP 程式碼編寫的簡單多線程類將會完美運行。
https://github.com/php/php-src/issues/8960

<?php

declare(ticks=1);

class
Thread {
protected static
$names = [];
protected static
$fibers = [];
protected static
$params = [];

public static function
register(string|int $name, callable $callback, array $params)
{
self::$names[] = $name;
self::$fibers[] = new Fiber($callback);
self::$params[] = $params;
}

public static function
run() {
$output = [];

while (
self::$fibers) {
foreach (
self::$fibers as $i => $fiber) {
try {
if (!
$fiber->isStarted()) {
// 註冊一個新的 tick 函式來排程這個 fiber
register_tick_function('Thread::scheduler');
$fiber->start(...self::$params[$i]);
} elseif (
$fiber->isTerminated()) {
$output[self::$names[$i]] = $fiber->getReturn();
unset(
self::$fibers[$i]);
} elseif (
$fiber->isSuspended()) {
$fiber->resume();
}
} catch (
Throwable $e) {
$output[self::$names[$i]] = $e;
}
}
}

return
$output;
}

public static function
scheduler () {
if(
Fiber::getCurrent() === null) {
return;
}

// 在此 if 條件中執行 Fiber::suspend() 將防止無限迴圈!
if(count(self::$fibers) > 1)
{
Fiber::suspend();
}
}
}

?>

以下是如何使用上述 Thread 類別的範例程式碼

<?php

// 定義一個非阻塞的執行緒,因此可以使用上述 Thread 類別以並行模式執行多個呼叫。
function thread (string $print, int $loop)
{
$i = $loop;
while (
$i--){
echo
$print;
}

return
"執行緒 '{$print}' 在印出 '{$print}' {$loop} 次後完成!";
}

// 註冊 6 個執行緒 (A, B, C, D, E 和 F)
foreach(range('A', 'F') as $c) {
Thread::register($c, 'thread', [$c, rand(5, 20)]);
}

// 執行執行緒並等待執行完成
$outputs = Thread::run();

// 印出輸出結果
echo PHP_EOL, '-------------- 傳回值 --------------', PHP_EOL;
print_r($outputs);

?>

輸出結果會類似這樣(但可能不同)

ABCDEFABCDEFABCDEFABCDEFABCDEFABCEFABFABFABEBEFBEFEFEFAABEABEBEFBEFFAAAAAA
-------------- 傳回值 --------------
陣列
(
[D] => 執行緒 'D' 在印出 'D' 5 次後完成!
[C] => 執行緒 'C' 在印出 'C' 6 次後完成!
[E] => 執行緒 'E' 在印出 'E' 15 次後完成!
[B] => 執行緒 'B' 在印出 'B' 15 次後完成!
[F] => 執行緒 'F' 在印出 'F' 15 次後完成!
[A] => 執行緒 'A' 在印出 'A' 18 次後完成!
)
nesk at xakep dot ru
2 年前
我認為在某些情況下,為了方便起見,將 Fiber 轉換為 Generator (Coroutine) 是有意義的。在這種情況下,這段程式碼將會很有用

<?php
function fiber_to_coroutine(\Fiber $fiber): \Generator
{
$index = -1; // 注意:前置遞增比後置遞增更快。
$value = null;

// 允許已在執行的 Fiber。
if (!$fiber->isStarted()) {
$value = yield ++$index => $fiber->start();
}

// 沒有暫停的 Fiber 應立即返回結果。
if (!$fiber->isTerminated()) {
while (
true) {
$value = $fiber->resume($value);

// 最後一次呼叫 "resume()" 會將 Fiber 的執行移至 "return" 陳述式。
//
// 因此不需要 "yield"。跳過此步驟並返回
// 結果。
if ($fiber->isTerminated()) {
break;
}

$value = yield ++$index => $value;
}
}

return
$fiber->getReturn();
}
?>
maxpanchnko at gmail dot com
2 年前
一個如何使用 Fibers 將 multi_curl 速度提高兩倍的範例 (虛擬碼)

<?php

$curlHandles
= [];
$urls = [
'https://example.com/1',
'https://example.com/2',
...
'https://example.com/1000',
];
$mh = curl_multi_init();
$mh_fiber = curl_multi_init();

$halfOfList = floor(count($urls) / 2);
foreach (
$urls as $index => $url) {
$ch = curl_init($url);
$curlHandles[] = $ch;

// half of urls will be run in background in fiber
$index > $halfOfList ? curl_multi_add_handle($mh_fiber, $ch) : curl_multi_add_handle($mh, $ch);
}

$fiber = new Fiber(function (CurlMultiHandle $mh) {
$still_running = null;
do {
curl_multi_exec($mh, $still_running);
Fiber::suspend();
} while (
$still_running);
});

// run curl multi exec in background while fiber is in suspend status
$fiber->start($mh_fiber);

$still_running = null;
do {
$status = curl_multi_exec($mh, $still_running);
} while (
$still_running);

do {
/**
* at this moment curl in fiber already finished (maybe)
* so we must refresh $still_running variable with one more cycle "do while" in fiber
**/
$status_fiber = $fiber->resume();
} while (!
$fiber->isTerminated());

foreach (
$curlHandles as $index => $ch) {
$index > $halfOfList ? curl_multi_remove_handle($mh_fiber, $ch) : curl_multi_remove_handle($mh, $ch);
}
curl_multi_close($mh);
curl_multi_close($mh_fiber);
?>
newuser
2 年前
展示 Fiber 和 Generator 之間差異的相同功能範例
<?php
$gener
= (function () use (&$gener): Generator {
$userfunc = function () use (&$gener) : Generator {
register_shutdown_function(function () use (&$gener) {
$gener->send('test');
});
return yield
'test';
};
$parm = yield from $userfunc();
echo
"用於恢復協程的值: ", $parm, PHP_EOL;
})();

$res = $gener->current();
echo
"協程暫停時的值: ", $res, PHP_EOL;
?>
<?php
$fiber
= new Fiber(function () use (&$fiber) : void {
$userfunc = function () use (&$fiber) : string {
register_shutdown_function(function () use (&$fiber) {
$fiber->resume('test');
});
return
Fiber::suspend('fiber');
};
$parm = $userfunc();
echo
"用於恢復Fiber的值: ", $parm, PHP_EOL;
});

$res = $fiber->start();
echo
"Fiber暫停時的值: ", $res, PHP_EOL;
?>
nikiDOTamministratoreATgmail at no dot spam
3 個月前
簡而言之

上面 Ali Madabi 的 Thread 類別最終已被連結的問題棄用,因為依賴 tick 函數進行搶先式多執行緒模擬已被認為是「不良做法」。建議使用更好的方法來實現某種多執行緒,例如:Revolt 和 AMP。

https://github.com/php/php-src/issues/8960#issuecomment-1184249445
To Top