Skip to content

Fiber 与异步编程

概述

Fiber 的引入为 PHP 原生异步编程奠定了基础。通过 Fiber,开发者可以在不依赖 Swoole 等扩展的情况下实现协程风格的异步 I/O 操作。本章将探讨 Fiber 在异步 I/O 中的应用、事件循环模式、与 Swoole Fiber 的对比,以及 PHP 原生异步生态的现状。

生态现状

ReactPHP 和 Amp 等主流 PHP 异步框架已经开始利用 Fiber 重写底层,大幅提升了异步代码的可读性和性能。

基础概念

Fiber 在异步 IO 中的角色

Fiber 本身不提供异步 I/O 能力,但它提供了一种机制,使得异步 I/O 操作可以用同步风格的代码来编写。当遇到 I/O 操作时,Fiber 暂停执行,将控制权交还给事件循环;当 I/O 操作完成时,事件循环恢复 Fiber 继续执行。

事件循环模式

事件循环是异步编程的核心。它不断检查 I/O 事件(文件可读、套接字可写等),当事件就绪时恢复对应的 Fiber。

PHP 原生异步生态

  • ReactPHP — 事件驱动编程框架,已支持 Fiber
  • Amp — 协程和异步编程库,基于 Fiber 重写
  • Revolt — 基于 Fiber 的事件循环,由 Amp 团队开发

语法与代码

基于 Fiber 的简单事件循环

php
<?php
declare(strict_types=1);

class AsyncEventLoop
{
    /** @var array<int, resource> */
    private array $readStreams = [];
    /** @var array<int, \Fiber> */
    private array $readFibers = [];
    private int $nextId = 1;

    public function readFile(string $path): \Generator
    {
        $stream = fopen($path, 'r');
        if ($stream === false) {
            throw new \RuntimeException("无法打开文件: {$path}");
        }

        while (!feof($stream)) {
            $data = fread($stream, 8192);
            if ($data === false || $data === '') {
                // 模拟异步等待
                yield;
                continue;
            }
            yield $data;
        }

        fclose($stream);
    }

    public function readFileAsync(string $path): string
    {
        return \Fiber::suspend(fn() => $this->readFile($path));
    }
}

模拟异步文件操作

php
<?php
declare(strict_types=1);

class SimpleAsync
{
    private static array $tasks = [];

    public static function async(callable $task): \Fiber
    {
        $fiber = new \Fiber(function () use ($task): void {
            $result = $task();
            self::$tasks[] = ['type' => 'result', 'value' => $result];
        });
        return $fiber;
    }

    public static function await(\Fiber $fiber): mixed
    {
        $fiber->start();
        return Fiber::suspend();
    }
}

// 模拟异步读取文件
$readFile = function (string $path): string {
    $content = file_get_contents($path);
    Fiber::suspend($content);
    return $content;
};

$fiber = new \Fiber(function () use ($readFile): void {
    $content = $readFile('/etc/hosts');
    echo "文件内容长度: " . strlen($content) . "\n";
});

$result = $fiber->start();
$fiber->resume();

使用 ReactPHP 风格的异步模式

php
<?php
declare(strict_types=1);

class MiniEventLoop
{
    /** @var \SplQueue<callable> */
    private \SplQueue $queue;
    private bool $running = false;

    public function __construct()
    {
        $this->queue = new \SplQueue();
    }

    public function defer(callable $callback): void
    {
        $this->queue->enqueue($callback);
        if (!$this->running) {
            $this->run();
        }
    }

    public function run(): void
    {
        $this->running = true;
        while (!$this->queue->isEmpty()) {
            $callback = $this->queue->dequeue();
            $callback();
        }
        $this->running = false;
    }
}

$loop = new MiniEventLoop();

$loop->defer(function () use ($loop): void {
    echo "任务 1 开始\n";

    $loop->defer(function () use ($loop): void {
        echo "  子任务 1.1\n";
    });

    $loop->defer(function (): void {
        echo "  子任务 1.2\n";
    });
});

$loop->defer(function (): void {
    echo "任务 2 开始\n";
});

// 不会立即执行,因为已经在 run() 中

Fiber 实现 sleep 替代

php
<?php
declare(strict_types=1);

class AsyncSleep
{
    /** @var array<int, array{float, \Fiber}> */
    private static array $sleepers = [];

    public static function sleep(float $seconds): void
    {
        $wakeTime = microtime(true) + $seconds;
        self::$sleepers[] = [$wakeTime, \Fiber::getCurrent()];
        \Fiber::suspend();
    }

    public static function tick(): void
    {
        $now = microtime(true);
        foreach (self::$sleepers as $i => [$wakeTime, $fiber]) {
            if ($now >= $wakeTime) {
                unset(self::$sleepers[$i]);
                $fiber->resume();
            }
        }
        self::$sleepers = array_values(self::$sleepers);
    }

    public static function hasSleepers(): bool
    {
        return !empty(self::$sleepers);
    }
}

$fiber = new \Fiber(function (): void {
    echo "开始: " . date('H:i:s') . "\n";
    AsyncSleep::sleep(0.001); // 模拟短暂等待
    echo "结束: " . date('H:i:s') . "\n";
});

$fiber->start();
usleep(1000);
AsyncSleep::tick();

详细说明

Swoole Fiber 对比

特性PHP 原生 FiberSwoole Fiber
安装方式内置(PHP 8.1+)扩展安装
异步 IO需自行实现或使用框架内置(Socket、File、HttpClient)
性能用户态,纯 PHP内核态优化
生态ReactPHP/Amp/RevoltSwoole 框架
协程调度手动(用户代码)自动(框架调度)
适用场景轻量级异步高性能服务器

选择建议

对于简单的异步需求,使用 PHP 原生 Fiber + ReactPHP/Amp。对于高性能 HTTP 服务器和协程调度,Swoole 仍然是更成熟的选择。

PHP 原生异步生态发展

PHP 8.1 Fiber 的引入推动了异步生态的变革:

  1. ReactPHP — 正在逐步采用 Fiber 简化异步 API
  2. Amp — 已完全基于 Fiber 重写,提供更自然的异步编程体验
  3. Revolt — 全新的事件循环实现,专为 Fiber 设计

异步模式 vs 同步模式

php
<?php
declare(strict_types=1);

// 同步模式(阻塞)
function fetchSync(string $url): string
{
    return file_get_contents($url);
}

// 异步模式(基于 Fiber,伪代码)
function fetchAsync(string $url): string
{
    // 在实际应用中,这里会使用事件循环注册 I/O 事件
    return Fiber::suspend();
}

实战示例

实战:异步 HTTP 客户端模式

php
<?php
declare(strict_types=1);

class AsyncHttpClient
{
    private \MiniEventLoop $loop;

    public function __construct(\MiniEventLoop $loop)
    {
        $this->loop = $loop;
    }

    public function get(string $url): \Fiber
    {
        return new \Fiber(function () use ($url): void {
            echo "请求: {$url}\n";

            // 模拟异步 HTTP 请求
            Fiber::suspend('requesting');

            echo "响应: {$url} - 200 OK\n";
            Fiber::suspend('response data');
        });
    }
}

class MiniEventLoop
{
    /** @var \SplQueue<\Fiber> */
    private \SplQueue $fiberQueue;

    public function __construct()
    {
        $this->fiberQueue = new \SplQueue();
    }

    public function schedule(\Fiber $fiber, mixed $value = null): void
    {
        $this->fiberQueue->enqueue([$fiber, $value]);
    }

    public function run(): void
    {
        while (!$this->fiberQueue->isEmpty()) {
            [$fiber, $value] = $this->fiberQueue->dequeue();

            if ($fiber->status() === \Fiber::STATUS_SUSPENDED) {
                $result = $fiber->resume($value);
            } elseif ($fiber->status() === \Fiber::STATUS_INIT) {
                $result = $fiber->start($value);
            }

            if ($fiber->status() === \Fiber::STATUS_SUSPENDED) {
                // 模拟 I/O 完成后重新调度
                $this->schedule($fiber, 'completed');
            }
        }
    }
}

$loop = new MiniEventLoop();
$client = new AsyncHttpClient($loop);

$req1 = $client->get('https://api.example.com/users');
$req2 = $client->get('https://api.example.com/posts');

$loop->schedule($req1);
$loop->schedule($req2);
$loop->run();

实战:协程风格的并发任务

php
<?php
declare(strict_types=1);

function asyncGather(callable ...$tasks): array
{
    $results = [];
    $fibers = [];

    foreach ($tasks as $i => $task) {
        $fibers[$i] = new \Fiber(function () use ($task, &$results, $i): void {
            $results[$i] = $task();
        });
        $fibers[$i]->start();
    }

    // 交错执行
    $running = true;
    while ($running) {
        $running = false;
        foreach ($fibers as $fiber) {
            if ($fiber->status() === \Fiber::STATUS_SUSPENDED) {
                $fiber->resume();
                $running = true;
            }
        }
    }

    return $results;
}

// 并发执行多个任务
$tasks = [
    fn(): string => Fiber::suspend() ?? 'Task A done',
    fn(): string => Fiber::suspend() ?? 'Task B done',
    fn(): string => Fiber::suspend() ?? 'Task C done',
];

$results = asyncGather(...$tasks);
print_r($results);

注意事项

Fiber 不是魔法

Fiber 本身不提供 I/O 多路复用或真正的异步 I/O。它只是提供了一种让代码在执行过程中暂停和恢复的机制。真正的异步 I/O 仍然需要底层的事件循环和 I/O 多路复用支持。

调试异步代码的困难

异步代码的执行流程不像同步代码那样直观,调试时需要特别注意 Fiber 之间的切换点和数据流动。

最佳实践

  1. 使用成熟的异步框架:除非有特殊需求,否则优先使用 ReactPHP、Amp 等成熟框架。
  2. 保持 Fiber 的独立性:每个 Fiber 应该尽可能独立,避免共享可变状态。
  3. 限制 Fiber 的生命周期:确保 Fiber 在合理的时间内完成,避免无限等待。
  4. 使用类型安全:为 Fiber 的 suspend/resume 传递值添加类型提示。
  5. 监控 Fiber 状态:在生产环境中监控 Fiber 的创建、暂停和完成情况。
php
<?php
declare(strict_types=1);

// 推荐模式:使用框架的异步 API
// 以 Amp 为例(伪代码)
// use Amp\ByteStream;

// $content = ByteStream\buffer(ByteStream\get('https://example.com'));
// echo $content;

参考链接