了解 PHP 8.1 fibers

2021 年 3 月,PHP 核心团队开发人员 Aaron Piotrowski 和 Niklas Keller 发布了一份征求意见稿(RFC),概述了在 PHP 语言核心中加入对纤维(fibers)的支持的理由。该 RFC 于月底获得批准,现已在即将发布的 PHP 8.1 版本中实施。

纤维(fiber)实现是低层次的,这意味着它主要被设计为 ReactPHP 或 Amp 等 PHP 异步框架或 Swoole 扩展等扩展的一部分。从 PHP 8.1 及以后版本开始,它将成为 PHP 语言的核心部分,因此开发人员无需过多考虑加载哪些扩展。此外,这也大大增强了 PHP 异步框架,因为它们现在可以直接在语言核心中获得底层支持,从而大大提高了性能。现在让我们来看看 Fiber 类本身。

探索 Fiber 类

PHP 8.1 的 Fiber 类提供了一个基本实现,异步框架和扩展开发人员可以在此基础上构建定时器、事件循环、promises和其他异步工件。

以下是该类的正式定义:

final class Fiber {
    public function __construct(callable $callback) {}
    public function start(mixed ...$args): mixed {}
    public function resume(mixed $value = null): mixed {}
    public function throw(Throwable $exception): mixed {}
    public function isStarted(): bool {}
    public function isSuspended(): bool {}
    public function isRunning(): bool {}
    public function isTerminated(): bool {}
    public function getReturn(): mixed {}
    public static function this(): ?self {}
    public static function suspend(
        mixed $value = null): mixed {}
}

以下是 Fiber 类方法的摘要:

image 2023 11 24 19 06 06 003
Figure 1. Table 12.3 – Fiber class method summary

如表 12.3 所示,创建 Fiber 实例后,使用 start() 运行与 fiber 相关的回调。之后,您可以使用 throw() 暂停、恢复或导致 fiber 失败。您也可以让回调在自己的 fiber 中运行,并使用 getReturn() 来获取返回的信息。您可能还会注意到,is*() 方法可用于确定 fiber 在任何给定时刻的状态。

有关 PHP 8.1 纤维实现的更多信息,请查看以下 RFC: https://wiki.php.net/rfc/fibers

现在让我们看一个说明 fibers 使用的示例。

使用 fibers

PHP 8.1 纤维是 PHP 异步应用程序的基础。虽然纤维的主要受众是框架和扩展开发人员,但任何 PHP 开发人员都可以从这一课程中受益。为了说明 PHP 纤维可以解决的问题,让我们来看一个简单的例子。

定义一个执行阻塞操作的示例程序

在这个使用同步编程模型编写的示例中,我们执行了以下三个操作:

  • 执行 HTTP GET 请求。

  • 执行数据库查询。

  • 向访问日志写入信息

由于已经掌握了一些异步编程的知识,你会意识到这三个任务都是阻塞操作。下面是我们要采取的步骤:

  1. 首先,我们定义一个要包含的 PHP 文件来定义回调:

    // /repo/ch12/php8_fibers_include.php
    define('WAR_AND_PEACE', 'https://www.gutenberg.org/files/2600/2600-0.txt');
    define('DB_FILE', __DIR__ . '/../sample_data/geonames.db');
    define('ACCESS_LOG', __DIR__ . '/access.log');
    $callbacks = [
        'read_url' => function (string $url) {
            return file_get_contents($url);
        },
        'db_query' => function (string $iso2) {
            $pdo = new PDO('sqlite:' . DB_FILE);
            $sql = 'SELECT * FROM geonames ' . 'WHERE country_code = ?';
            $stmt = $pdo->prepare($sql);
            $stmt->execute([$iso2]);
            return var_export($stmt->fetchAll(PDO::FETCH_ASSOC), TRUE);
        },
        'access_log' => function (string $info) {
            $info = date('Y-m-d H:i:s') . ": $info\n";
            return file_put_contents(
                ACCESS_LOG, $info, FILE_APPEND);
        },
    ];
    return $callbacks;
  2. 接下来,我们定义一个包含回调定义并按顺序执行的 PHP 程序。我们使用 PHP 8 中的匹配 {} 结构来分配不同的参数传递给相应的回调。最后,我们只需返回一个字符串并运行 strlen() 即可返回回调生成的字节数:

    // /repo/ch12/php8_fibers_blocked.php
    $start = microtime(TRUE);
    $callbacks = include __DIR__ . '/php8_fibers_include.php';
    foreach ($callbacks as $key => $exec) {
        $info = match ($key) {
            'read_url' => WAR_AND_PEACE,
            'db_query' => 'IN',
            'access_log' => __FILE__,
            default => ''
        };
        $result = $exec($info);
        echo "Executing $key" . strlen($result) . "\n";
    }
    echo "Elapsed Time:" . (microtime(TRUE) - $start) . "\n";

如果我们按原样运行该程序,结果可想而知会非常糟糕,正如我们在这里看到的那样:

root@php8_tips_php8_1 [ /repo/ch12 ]#
php php8_fibers_blocked.php
Executing read_url: 3359408
Executing db_query: 23194
Executing access_log: 2
Elapsed Time:6.0914640426636

下载托尔斯泰《战争与和平》的统一资源定位器(URL)请求耗时最长,产生的字节数超过 300 万。总耗时略高于 6 秒。

现在让我们看看如何使用光纤重写调用程序。

使用纤维的示例程序

在 PHP 8.1 Docker 容器中,我们可以定义一个使用光纤的调用程序。具体步骤如下:

  1. 首先,我们像之前一样加入回调,就像这样:

    // /repo/ch12/php8_fibers_unblocked.php
    $start = microtime(TRUE);
    $callbacks = include __DIR__ . '/php8_fibers_include.php';
  2. 接下来,我们创建一个 Fiber 实例来封装每个回调。然后,我们使用 start() 启动回调,并提供相应的信息:

    $fibers = [];
    foreach ($callbacks as $key => $exec) {
        $info = match ($key) {
            'read_url' => WAR_AND_PEACE,
            'db_query' => 'IN',
            'access_log' => __FILE__,
            default => ''
        };
        $fibers[$key] = new Fiber($exec);
        $fibers[$key]->start($info);
    }
  3. 然后,我们建立一个循环,检查每个回调是否完成。如果是,我们会回显 getReturn() 的结果,并取消设置光纤:

    $count = count($fibers);
    $names = array_keys($fibers);
    while ($count) {
        $count = 0;
        foreach ($names as $name) {
            if ($fibers[$name]->isTerminated()) {
                $result = $fibers[$name]->getReturn();
                echo "Executing $name: \t"
                    . strlen($result) . "\n";
                unset($names[$name]);
            } else {
                $count++;
            }
        }
    }
    echo "Elapsed Time:" . (microtime(TRUE) - $start) . "\n";

请注意,本示例仅供参考。您更有可能使用的是 ReactPHP 或 Amp 等现有框架,这两种框架都是为了利用 PHP 8.1 纤维而重写的。还需要注意的是,即使同时运行多个光纤,您所能达到的最短运行时间也与运行时间最长的任务所耗费的时间成正比。现在让我们看看纤维对 ReactPHP 和 Swoole 的影响。

检查 Fiber 对 ReactPHP 和 Swoole 的影响

在本示例中,您需要在 PHP 8.1 Docker 容器中分别打开两个命令 shell。请按照上一节的说明操作,但要打开两个命令 shell 而不是一个。然后,我们将使用 /repo/ch12/php8_chat_test.php 程序来测试光纤的效果。让我们使用内置的 PHP 网络服务器作为控制,运行第一个测试。

使用内置 PHP 网络服务器进行测试

在第一次测试中,我们使用内置的 PHP 网络服务器和传统的 /repo/ch12/ php8_chat_ajax.php 实现。以下是我们要采取的步骤:

  1. 在两个命令 shell 中,切换到 /repo/ch12 目录,如下所示:

    # cd /repo/ch12
  2. 在第一个命令 shell 中,使用内置的 PHP 网络服务器运行标准 HTTP 服务器,命令如下:

    # php -S localhost:9501 php8_chat_ajax.php
  3. 在第二个命令 shell 中执行测试程序,如图所示:

    php php8_chat_test.php http://localhost:9501 1000 --no

输出结果应该是这样的:

root@php8_tips_php8_1 [ /repo/ch12 ]#
php php8_chat_test.php http://localhost:9501 1000 --no
From User: pduarte
Elapsed Time: 1.687940120697

正如您所看到的,使用同步编程编写的传统代码,迭代 1000 次的耗时约为 1.7 秒。现在让我们看看使用 ReactPHP 运行相同测试的情况。

使用 ReactPHP 进行测试

在第二个测试中,我们使用 /repo/ch12/php8_chat_react.php ReactPHP 实现。以下是我们要采取的步骤:

  1. 在第一个命令 shell 中,按 Ctrl + C 退出内置的 PHP 网络服务器。

  2. 使用 exit 退出并重新进入第一个命令 shell,然后使用 Windows 的 init shell 或 Linux 或 Mac 的 ./init.sh shell

  3. 使用此命令启动 ReactPHP 服务器:

    # php php8_chat_react.php
  4. 在第二个命令 shell 中执行测试程序,如下所示:

    php php8_chat_test.php http://localhost:9501 1000 --no

输出结果应该是这样的:

root@php8_tips_php8_1 [ /repo/ch12 ]#
php php8_chat_test.php http://localhost:9501 1000 --no
From User: klang
Elapsed Time: 1.2330160140991

从输出结果可以看出,ReactPHP 从纤维中受益匪浅。迭代 1000 次的总耗时仅为 1.2 秒,令人印象深刻!

关于 PHP 8.1 纤维的讨论到此结束。您现在已经了解了什么是纤维、如何在程序代码中直接使用纤维,以及纤维如何使外部 PHP 异步框架受益。

总结

在本章中,你了解了传统同步编程和异步编程的区别。本章还涉及事件循环、定时器、承诺和通道等关键术语。掌握了这些知识,您就能确定何时使用异步编程模型编写代码块,以及如何重写现有同步模型应用程序的部分内容以利用异步功能。

然后,您还了解了 Swoole 扩展,以及如何将其应用于现有的应用程序代码以提高性能。您还了解了其他一些以异步方式运行的框架和扩展。您还查看了具体的代码示例,现在已经开始编写异步代码了。

在上一节中,我们介绍了 PHP 8.1 纤维。然后,我们回顾了一个代码示例,向您展示了如何使用 PHP 8.1 纤维创建协同多任务函数和类方法。此外,您还将了解某些 PHP 异步框架是如何从 PHP 8.1 光纤支持中获益,从而带来更多性能改进的。

这是本书的最后一章。希望您喜欢回顾 PHP 8 中提供的大量新特性和新优势。 现在,您对面向对象代码和过程代码中需要避免的潜在陷阱,以及 PHP 8 扩展的各种变化有了更深入的了解。有了这些知识,您现在不仅可以编写出更好的代码,而且还可以制定一个可靠的行动计划,最大限度地降低 PHP 8 移植后应用程序代码失败的几率。