深入理解PHP中的纤程(Fiber):揭秘异步编程的底层实现

2023-10-22 18:57:14 浏览数 (2)

纤程概述

PHP 8.1 引入了 Fiber,这是一个低级功能,用于从调用堆栈中的任何位置暂停和恢复函数执行,本质上支持运行时处理的协程。

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

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

Fiber 理论

In essence, a Fiber is a code block that maintains its own stack (variables and state), that can be started, suspended, or terminated cooperatively by the main code and the Fiber.

从本质上讲,Fiber 是一个维护自己的堆栈(变量和状态)的代码块,可以通过主代码和 Fiber 协同启动、挂起或终止。

Fibers are similar to threads in a computer program. Threads are scheduled by the operating system, and does not guarantee when and at which point the threads are paused and resumed. Fibers are created, started, suspended, and terminated by the program itself, and allows fine control of the main program execution and the Fiber execution.

纤维类似于计算机程序中的线程。线程由操作系统调度,不保证线程在何时何地暂停和恢复。纤程由程序本身创建、启动、挂起和终止,并允许对主程序执行和纤程执行进行精细控制。

PHP 5.4 added Generators to PHP. With Generators, it was possible to yield a Generator instance back to the caller, without deleting the state of the code block. Generators did not allow the call to be easily resumed from the point of the code block that yield was called.

PHP 5.4 在 PHP 中添加了生成器。使用生成器,可以将 yield 生成器实例返回到调用方,而无需删除代码块的状态。生成器不允许从 yield 调用的代码块点轻松恢复调用。

With Fibers, the code block within the Fiber can suspend the code block and return any data back to the main program. The main program can resume the Fiber from the point it was suspended.

使用 Fibers,Fiber 中的代码块可以挂起代码块并将任何数据返回给主程序。主程序可以从光纤挂起的位置恢复光纤。

It is important the concurrent execution does not mean simultaneous execution. The Fiber and the main execution flow does not happen at the same time. It is up to the main execution flow to start a Fiber, and when it starts, the Fiber is executed exclusively. The main thread cannot observe, terminate, or suspend a Fiber while the Fiber is being executed. The Fiber can suspend itself, and it cannot resume by itself — the main thread must resume the Fiber.

重要的是,并发执行并不意味着同时执行。光纤和主执行流不会同时发生。启动 Fiber 由主执行流程决定,当它启动时,Fiber 以独占方式执行。主线程无法在执行光纤时观察、终止或挂起光纤。光纤可以自行挂起,也不能自行恢复 — 主线程必须恢复光纤。

Fiber by itself does not allow simultaneous execution of multiple Fibers or the main thread and a Fiber.

光纤本身不允许同时执行多个光纤或主线和光纤。

Fiber 类

PHP Fibers are implemented around a new class called Fiber. This class is declared final, which prevents it from being extended by another user-land class.

PHP Fibers 是围绕一个名为 的新 Fiber 类实现的。这个类被声明 final ,这可以防止它被另一个用户土地类扩展。

Fiber Class

代码语言:javascript复制
final class Fiber {
    /* 方法 */
    public __construct(callable $callback)
    public start(mixed ...$args): mixed
    public resume(mixed $value = null): mixed
    public throw(Throwable $exception): mixed
    public getReturn(): mixed
    public isStarted(): bool
    public isSuspended(): bool
    public isRunning(): bool
    public isTerminated(): bool
    public static suspend(mixed $value = null): mixed
    public static getCurrent(): ?Fiber
}

Fiber Class Methods

  • Fiber::__construct — 创建新的 Fiber 实例
  • Fiber::start — 启动 fiber 的执行
  • Fiber::resume — 使用值恢复 fiber 的执行
  • Fiber::throw — 用一个异常来恢复 fiber 的执行
  • Fiber::getReturn — 获取 Fiber 的返回值
  • Fiber::isStarted — 确定 fiber 是否启动
  • Fiber::isSuspended — 确认 fiber 是否挂起
  • Fiber::isRunning — 确认 fiber 是否正在运行
  • Fiber::isTerminated — 确认 fiber 是否终止
  • Fiber::suspend — 暂停当前 fiber 的执行
  • Fiber::getCurrent — 获取当前正在执行的 Fiber 实例

Fiber::__construct()

实例化新 Fiber 类实例时,调用方必须传递有效的可调用对象。 use 局部变量也可以在范围内使用。

代码语言:javascript复制
new Fiber('var_dump');
new Fiber(fn(string $message) => print $message);
new Fiber(function(string $message): void {
    print $message;
});

The parameters of the callback will receive the exact same parameters that the Fiber::start() method is called with.

回调的参数将接收与调用 Fiber::start() 该方法时使用的完全相同的参数。

Fiber::start() 启动光纤

创建光纤后,不会立即启动。

方法调用将 Fiber::start() 启动 中 Fiber::construct 设置的回调。传递给 Fiber::start 方法的所有值都将传递给回调。

代码语言:javascript复制
$fiber = new Fiber(fn(string $message) => print $message);
$fiber->start('Hi');

输出 Hi

Fiber::suspend() 暂停正在运行的光纤

Fiber::suspend() 是一种 static 方法,只能从光纤内部调用。它可以选择性地返回一个值,调用方 Fiber::start() Fiber::resume() 或可以接收该值。

代码语言:javascript复制
$fiber = new Fiber(function() {
    Fiber::suspend(42);
});
$return = $fiber->start();
echo $return;

输出 ``

调用时 Fiber::suspend() ,光纤在该表达式处暂停。在从内存中删除 Fiber 对象本身之前,不会清除局部变量、数组指针等。下一个调用从下一个 Fiber::resume 表达式继续程序。

从光纤外部调用 Fiber::suspend() 会引发 FiberError 异常:

代码语言:javascript复制
$fiber = new Fiber(function() {});
Fiber::suspend();
代码语言:javascript复制
FiberError: Cannot suspend outside of a fiber in ...:...

Fiber::resume() 恢复挂起的光纤

可以使用该方法 Fiber::resume 恢复挂起(使用 Fiber::suspend )的光纤。

代码语言:javascript复制
$fiber = new Fiber(function() {
    echo "Suspending...n";
    Fiber::suspend();
    echo "Resumed!";
});
$fiber->start();
echo "Resuming...n";
$fiber->resume();

运行输出

代码语言:javascript复制
Suspending...
Resuming...
Resumed!

该方法 Fiber::resume 接受一个值,该值可以赋回 Fiber 作用域中最后一个 Fiber::suspend 返回值的返回值。

代码语言:javascript复制
$fiber = new Fiber(function() {
    $last = Fiber::suspend(16);
    echo "Resuming with last value {$last}n";
});
$last = $fiber->start();
echo "Suspended with last value {$last}n";
$fiber->resume(42);

运行输出

代码语言:javascript复制
Suspended with last value 16
Resuming with last value 42

调用未挂起或已终止的光纤会导致 Fiber::resume FiberError 。

代码语言:javascript复制
$fiber = new Fiber(function() {});
$fiber->resume();

发生错误

代码语言:javascript复制
FiberError: Cannot resume a fiber that is not suspended in ...:...

Fiber::getCurrent() 获取当前光纤实例

Fiber::getCurrent() static 方法返回当前正在运行的光纤实例。这很方便, callable 因为 $this 已经分配给实例本身。

使用 Fiber::getCurrent ,光纤可以使用 、 Fiber::getReturn 、 Fiber::throw Fiber::isStarted Fiber::isSuspended Fiber::isRunning 和 [ ] 等方法检查其自身的状态 Fiber::isTerminated 。

代码语言:javascript复制
$fiber = new Fiber(function(): int {
    return 42;
});
$fiber->start();
var_dump($fiber->getReturn());

输出

代码语言:javascript复制
int(42)

如果光纤回调没有 return , getReturn() 则方法返回 null 。

调用尚未终止或抛出可抛出的光纤会导致 getReturn FiberError 异常:

代码语言:javascript复制
$fiber = new Fiber(function(): void {
    Fiber::suspend();
});
$fiber->start();
$fiber->getReturn();

异常输出

代码语言:javascript复制
FiberError: Cannot get fiber return value: The fiber has not returned in ...:...

Fiber::throw() 抛出异常到纤维

Fiber::throw() 方法接受一个对象, Throwable 该对象恢复 Fiber,但也立即 throw 接受该异常。

如果调用 Fiber::suspend 的光纤可以选择捕获传递 Throwable 的 .

代码语言:javascript复制
$fiber = new Fiber(function(): void {
    try {
        Fiber::suspend();
    }
    catch (Exception $ex) {
        echo $ex->getMessage();
    }
    echo 'Finishing';
});
$fiber->start();
$fiber->throw(new Exception("Testn"));

运行输出

代码语言:javascript复制
Test
Finishing

如果 Fiber 回调没有捕获传递 Throwable 的对象,它将冒泡回调用方。

代码语言:javascript复制
$fiber = new Fiber(function(): void {
    Fiber::suspend();
    echo 'Finishing';
});
$fiber->start();
$fiber->throw(new Exception("Testn"));

// output Fatal error: Uncaught Exception: Test

Fiber::isStarted

返回光纤是否已启动(使用 Fiber::start )。即使光纤被暂停或终止,这也是正确的。

代码语言:javascript复制
$fiber = new Fiber(function(): void {});
$fiber->isStarted(); // false
$fiber->start();
$fiber->isStarted(); // true

Fiber::isSuspended

如果光纤当前已挂起,则返回 true 这种不言自明的方法。

代码语言:javascript复制
$fiber = new Fiber(function(): void {
    Fiber::suspend();
});

$fiber->isSuspended(); // false
$fiber->start();
$fiber->isSuspended(); // true
$fiber->resume();
$fiber->isSuspended(); // false

当前正在运行的光纤从 Fiber::isRunning 方法返回 true 。挂起和终止的状态将返回 false Fiber::isRunning 。

Fiber::isTerminated

返回光纤回调是否已结束。

代码语言:javascript复制
$fiber = new Fiber(function(): void {
    Fiber::suspend();
});

$fiber->isTerminated(); // false
$fiber->start();
$fiber->isTerminated(); // false
$fiber->resume();
$fiber->isTerminated(); // true

Fiber Exceptions

PHP 8.1 中的光纤功能添加了两个新 Throwable 类。它们都不能由用户空间的 PHP 代码实例化,因为它们的执行在其构造函数中受到限制。

FiberError

代码语言:javascript复制
/**
 * Exception thrown due to invalid fiber actions, such as resuming a terminated fiber.
 */
final class FiberError extends Error
{
    /**
     * Constructor throws to prevent user code from throwing FiberError.
     */
    public function __construct()
    {
        throw new Error('The "FiberError" class is reserved for internal use and cannot be manually instantiated');
    }
}

FiberExit

代码语言:javascript复制
/**
 * Exception thrown when destroying a fiber. This exception cannot be caught by user code.
 */
final class FiberExit extends Exception
{
    /**
     * Constructor throws to prevent user code from throwing FiberExit.
     */
    public function __construct()
    {
        throw new Error('The "FiberExit" class is reserved for internal use and cannot be manually instantiated');
    }
}

使用示例

光纤允许运行并发执行代码,光纤可以随时暂停这些代码,也可以选择返回值。从主线程,可以准确地从上次挂起的位置恢复挂起的光纤。

请注意,PHP 8.1 中添加的 Fibers 仅用于并发,但它不支持并行处理。例如,它不允许同时运行两个 Curl 文件下载。光纤可以作为并行处理事件循环的底层结构,轻松管理程序状态

一个简单的回声程序

下面是一个显示执行流程的简单程序。

当被调用时 Fiber::suspend() ,光纤在表达式处挂起。此时也可以返回一个值。如果 Fiber 不调用 Fiber::suspend() 或 throw ,则执行该 Fiber,直到它到达回调结束。

恢复挂起/抛出的光纤完全取决于主程序。如果主程序退出,则丢弃所有剩余的光纤。

代码语言:javascript复制
$fiber = new Fiber(function(): void {
    echo "Hello from the Fiber...n";
    Fiber::suspend();
    echo "Hello again from the Fiber...n";
});

echo "Starting the program...n";
$fiber->start();
echo "Taken control back...n";
echo "Resuming Fiber...n";
$fiber->resume();
echo "Program exits...n";

输出:

代码语言:javascript复制
Starting the program...
Hello from the Fiber...
Taken control back...
Resuming Fiber...
Hello again from the Fiber...
Program exits...

带有进度条的文件复制程序

一个简单的回显示例可能不会显示 Fiber 的优点,因为它不返回或传递任何值。

使用Fibers,可以将文件列表复制到目标的简单程序变得更简洁。

代码语言:javascript复制
function writeToLog(string $message): void {
    echo $message . "n";
}
$files = [
    'src/foo.png' => 'dest/foo.png',
    'src/bar.png' => 'dest/bar.png',
    'src/baz.png' => 'dest/baz.png',
];

$fiber = new Fiber(function(array $files): void {
    foreach($files as $source => $destination) {
        copy($source, $destination);
        Fiber::suspend([$source, $destination]);
    }
});

// Pass the files list into Fiber.
$copied = $fiber->start($files);
$copied_count = 1;
$total_count  = count($files);

while(!$fiber->isTerminated()) {
    $percentage = round($copied_count / $total_count, 2) * 100;
    writeToLog("[{$percentage}%]: Copied '{$copied[0]}' to '{$copied[1]}'");
    $copied = $fiber->resume();
      $copied_count;
}

writeToLog('Completed');

运行输出

代码语言:javascript复制
[33%]: Copied 'src/foo.png' to 'dest/foo.png'
[67%]: Copied 'src/bar.png' to 'dest/bar.png'
[100%]: Copied 'src/baz.png' to 'dest/baz.png'
Completed

实际的文件复制操作在 Fiber 内部处理,Fiber 回调仅接受要复制的文件列表及其相应的目标。

复制文件后,光纤会将其挂起,并将源名称和目标名称返回给调用方。然后,调用方更新进度,并记录有关刚复制的文件的信息。

使用 while 环路,光纤恢复,直到它终止。如果 throw 无法继续,光纤可能会出现任何异常,并且它也将冒泡到主程序。

使用 Fiber 时,回调保持精简,因为它不需要处理其他操作,例如更新进度。

0 人点赞