ファイバー
ファイバーの概要
(PHP 8 >= 8.1.0)
ファイバー(Fibers) は、完全なスタックを持つ、停止可能な関数です。 ファイバー はコールスタック中のどこからでも停止することができますし、 後に再開されるまで実行を停止したままにできます。
ファイバーは実行スタック全体を停止するので、 関数を直接呼び出している側は、関数の呼び出し方を変更する必要はありません。
実行スタック中のどこからでも、 Fiber::suspend() を使って実行を停止できます。 (つまり、Fiber::suspend() の呼び出しは、 深くネストされた関数のコールスタックの中から行っても構いませんし、 呼び出しが全く存在しなくても構いません)
スタックを持たない Generator と異なり、 個々の Fiber は自分のコールスタックを持ちます。 これによって、深くネストされた関数のコールスタックの中からでも 関数を停止できます。 停止ポイントを宣言した (つまり、Fiber::suspend() を呼び出した) 関数は、 yield を使った、Generator のインスタンスを返さなければならない関数と異なり、 戻り値の型を宣言する必要はありません。
ファイバーは任意の関数をコールしている間に停止できます。 これには、PHP VM の中からコールされた関数も含みます。 たとえば、array_map() に渡した関数や、 foreach によってコールされる、Iterator オブジェクトのメソッドなどです。
いったんファイバーが停止されると、 ファイバーの実行は Fiber::resume() に任意の値を渡すか、 Fiber::throw() を使ってファイバーに例外をスローさせることで再開できます。 再開される時は、Fiber::suspend() から値が返されます(または、例外がスローされます)。
注意: 現状の制限により、 ファイバーをオブジェクトのデストラクタ中で切り替えることはできません。
例1 ファイバーの基本的な使い方
<?php
$fiber = new Fiber(function (): void {
$value = Fiber::suspend('fiber');
echo "Value used to resume fiber: ", $value, PHP_EOL;
});
$value = $fiber->start();
echo "Value from fiber suspending: ", $value, PHP_EOL;
$fiber->resume('test');
?>
上の例の出力は以下となります。
Value from fiber suspending: fiber Value used to resume fiber: test
User Contributed Notes 6 notes
Perhaps not using the same variable name everywhere will be a good idea
<?php
$fiber = new Fiber(function (): void {
$parm = Fiber::suspend('fiber');
echo "Value used to resume fiber: ", $parm, PHP_EOL;
});
$res = $fiber->start();
echo "Value from fiber suspending: ", $res, PHP_EOL;
$fiber->resume('test');
?>
Here is a simple scheduler and thread pool that implements multithreading using fibers and tick functions in PHP 8.1 and returns the return value of each function in the pool in an array at the end.
Note that due to some bugs, you need to register a new tick function for each "thread". Remember to unregister all of them at the end.
The link bellow is the discussion on a bug that is going on right now (At the time of writing this). Note that based on the discussion, the ability to call Fiber::suspend() inside tick function may become forbidden in PHP 8.2+. But if the bug gets fixed, you can move register_tick_function() line to the top of the class, and this simple multithreading class in pure PHP code will work like a charm.
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()) {
// Register a new tick function for scheduling this 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;
}
// running Fiber::suspend() in this if condition will prevent an infinite loop!
if(count(self::$fibers) > 1)
{
Fiber::suspend();
}
}
}
?>
And here is an example code on how to use above Thread class:
<?php
// defining a non-blocking thread, so multiple calls will run in concurrent mode using above Thread class.
function thread (string $print, int $loop)
{
$i = $loop;
while ($i--){
echo $print;
}
return "Thread '{$print}' finished after printing '{$print}' for {$loop} times!";
}
// registering 6 Threads (A, B, C, D, E, and F)
foreach(range('A', 'F') as $c) {
Thread::register($c, 'thread', [$c, rand(5, 20)]);
}
// run threads and wait until execution finishes
$outputs = Thread::run();
// print outputs
echo PHP_EOL, '-------------- RETURN VALUES --------------', PHP_EOL;
print_r($outputs);
?>
The output will be something like this (but probably different):
ABCDEFABCDEFABCDEFABCDEFABCDEFABCEFABFABFABEBEFBEFEFEFAABEABEBEFBEFFAAAAAA
-------------- RETURN VALUES --------------
Array
(
[D] => Thread 'D' finished after printing 'D' for 5 times!
[C] => Thread 'C' finished after printing 'C' for 6 times!
[E] => Thread 'E' finished after printing 'E' for 15 times!
[B] => Thread 'B' finished after printing 'B' for 15 times!
[F] => Thread 'F' finished after printing 'F' for 15 times!
[A] => Thread 'A' finished after printing 'A' for 18 times!
)
I think that in some cases it makes sense to convert a Fiber to a Generator (Coroutine) for convenience. In such cases, this code will be useful:
<?php
function fiber_to_coroutine(\Fiber $fiber): \Generator
{
$index = -1; // Note: Pre-increment is faster than post-increment.
$value = null;
// Allow an already running fiber.
if (!$fiber->isStarted()) {
$value = yield ++$index => $fiber->start();
}
// A Fiber without suspends should return the result immediately.
if (!$fiber->isTerminated()) {
while (true) {
$value = $fiber->resume($value);
// The last call to "resume()" moves the execution of the
// Fiber to the "return" stmt.
//
// So the "yield" is not needed. Skip this step and return
// the result.
if ($fiber->isTerminated()) {
break;
}
$value = yield ++$index => $value;
}
}
return $fiber->getReturn();
}
?>
One of examples, how to make multi_curl faster twice (pseudocode) using Fibers:
<?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);
?>
Example of the same functionality showing what is the difference between Fiber and 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 "Value used to resume fiber: ", $parm, PHP_EOL;
})();
$res = $gener->current();
echo "Value from fiber suspending: ", $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 "Value used to resume fiber: ", $parm, PHP_EOL;
});
$res = $fiber->start();
echo "Value from fiber suspending: ", $res, PHP_EOL;
?>
TL;DR
The Thread class from Ali Madabi above has been eventually deprecated by the linked issue as relaying on tick functions for preemptive multi-threading simulation has been deemed "bad practice". Better ways were suggested for achieving some sort of multi-threading, such as: Revolt and AMP.
https://github.com/php/php-src/issues/8960#issuecomment-1184249445