也許不要在所有地方都使用相同的變數名稱會是個好主意。
<?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');
?>
(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
也許不要在所有地方都使用相同的變數名稱會是個好主意。
<?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');
?>
這是一個簡單的排程器和線程池,它使用 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 次後完成!
)
我認為在某些情況下,為了方便起見,將 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();
}
?>
一個如何使用 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);
?>
展示 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;
?>
簡而言之
上面 Ali Madabi 的 Thread 類別最終已被連結的問題棄用,因為依賴 tick 函數進行搶先式多執行緒模擬已被認為是「不良做法」。建議使用更好的方法來實現某種多執行緒,例如:Revolt 和 AMP。
https://github.com/php/php-src/issues/8960#issuecomment-1184249445