纤程

纤程概述

(PHP 8 >= 8.1.0)

纤程(Fiber)表示一组有完整栈、可中断的功能。 纤程可以在调用堆栈中的任何位置被挂起,在纤程内暂停执行,直到稍后恢复。

纤程可以暂停整个执行堆栈,所以该函数的直接调用者不需要改变调用这个函数的方式。

你可以在调用堆栈的任意地方使用 Fiber::suspend() 中断执行(也就是说,Fiber::suspend() 的调用位置可以在一个深度嵌套的函数中,甚至可以不存在)。

与无栈的 Generator 不同, 每一个 Fiber 拥有自己的调用栈,并允许在一个深度前度的函数调用中将它们暂停。 声明了中断(interruption)点的函数(即调用 Fiber::suspend()) 不需要改变自己的返回类型,不像使用 yield 一样需要返回一个 Generator 实例。

纤程可以在任意函数调用中被暂停,包括那些在 PHP VM 中被调用的函数。 例如被用于 array_map() 的函数或者提供 Iterator 对象以被 foreach 调用的方法。

纤程一旦被暂停,可以使用 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
添加备注

用户贡献的备注 6 notes

up
89
user at csa dot es
2 years ago
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');
?>
up
34
Ali Madadi
2 years ago
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!
)
up
10
nesk at xakep dot ru
2 years ago
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();
}
?>
up
21
maxpanchnko at gmail dot com
2 years ago
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);
?>
up
5
newuser
2 years ago
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;
?>
up
1
nikiDOTamministratoreATgmail at no dot spam
3 months ago
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
To Top