协程

协程概述

(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() 返回(或抛出异常)。

注意: 由于当前限制,无法在对象的析构函数中切换协程。

示例 #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
添加笔记

用户贡献笔记 7 笔记

89
user at 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');
?>
33
Ali Madadi
2 年前
这里是一个简单的调度程序和线程池,它使用 PHP 8.1 中的协程和 tick 函数实现多线程,并在最后将每个函数池的返回值保存在一个数组中。

请注意,由于一些错误,你需要为每个“线程”注册一个新的 tick 函数。请记住在最后取消注册所有这些函数。

下面的链接是关于目前正在发生的错误的讨论(在撰写本文时)。请注意,根据讨论,在 PHP 8.2+ 中,调用 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()) {
// 为调度此 Fiber 注册一个新的 tick 函数
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 次后完成!
)
9
nesk at xakep dot ru
1 年前
我认为在某些情况下,为了方便起见,将 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();
}
?>
20
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);
?>
6
newuser
2 年前
示例展示了相同功能,并说明了协程和生成器之间的区别
<?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;
?>
0
nikiDOTamministratoreATgmail at no dot spam
8 天前
TL;DR

Ali Madabi 上面的 Thread 类最终被链接问题弃用,因为依赖于滴答函数来模拟抢占式多线程被认为是“不好的做法”。建议使用更好的方法来实现某种形式的多线程,例如:Revolt 和 AMP。

https://github.com/php/php-src/issues/8960#issuecomment-1184249445
-15
dvohra09 at yahoo dot com
1 年前
Fiber::isRunning 不返回值
示例
-------

<?php


$fiber
= new Fiber(function (): void {
$value = Fiber::suspend('suspend');

echo
"协程已恢复,值为: ", $value, "\n";

});

echo
"协程尚未启动.", "\n";

$value = $fiber->start();

echo
"协程已启动: ", $fiber->isStarted(), "\n";
echo
"协程已挂起: ", $fiber->isSuspended(), "\n";

echo
"协程正在运行: ", $fiber->isRunning(), "\n";

echo
"协程已挂起,值为: ", $value, "\n";
$fiber->resume('resume');

echo
"协程正在运行: ", $fiber->isRunning(), "\n";

?>
输出
---

协程尚未启动. 协程已启动: 1 协程已挂起: 1 协程正在运行: 协程已挂起,值为: suspend 协程已恢复,值为: resume 协程正在运行
To Top