PHP 大会日本 2024

纤程

纤程概述

(PHP 8 >= 8.1.0)

纤程表示全栈、可中断的函数。纤程可以在调用栈中的任何位置暂停,暂停纤程内的执行,直到稍后恢复纤程。

纤程会暂停整个执行栈,因此函数的直接调用方不需要更改其调用函数的方式。

可以使用 Fiber::suspend() 在调用栈中的任何位置中断执行(也就是说,对 Fiber::suspend() 的调用可能位于深度嵌套的函数中,或者根本不存在)。

与无栈 Generator 不同,每个 Fiber 都有自己的调用栈,允许它们在深度嵌套的函数调用中暂停。声明中断点的函数(即调用 Fiber::suspend())不需要更改其返回类型,这与使用 yield 的函数不同,后者必须返回 Generator 实例。

纤程可以在任何函数调用中暂停,包括从 PHP VM 内部调用的那些函数,例如提供给 array_map() 的函数或由 foreachIterator 对象上调用的方法。

暂停后,可以使用 Fiber::resume() 使用任何值恢复纤程的执行,或者通过使用 Fiber::throw() 将异常抛入纤程来恢复。该值从 Fiber::suspend() 返回(或抛出异常)。

注意在 PHP 8.4.0 之前,不允许在对象 析构函数 的执行过程中切换纤程。

示例 #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 条注释

csa dot es 的用户
2 年前
也许在任何地方都不使用相同的变量名是个好主意

<?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');
?>
Ali Madadi
2 年前
这是一个简单的调度程序和线程池,它使用 PHP 8.1 中的纤程和 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
"Thread '{$print}' finished after printing '{$print}' for {$loop} times!";
}

// 注册 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] => 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!
)
nesk at xakep dot ru
2 年前
我认为在某些情况下,为了方便起见,将 Fiber 转换为 Generator(协程)是有意义的。在这种情况下,这段代码将很有用

<?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 年前
使用 Fiber 使 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;

// 一半的URL将在协程的后台运行
$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);
});

// 在协程处于挂起状态时,在后台运行curl多执行
$fiber->start($mh_fiber);

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

do {
/**
* 此时协程中的curl可能已经完成
* 所以我们必须通过协程中的一轮额外的“do while”循环来刷新$still_running变量
**/
$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
"用于恢复协程的值: ", $parm, PHP_EOL;
});

$res = $fiber->start();
echo
"协程挂起时的值: ", $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