Skip to content

Fiber 概览

概述

Fiber(纤程)是 PHP 8.1 引入的一种轻量级并发机制,允许在单个线程内实现协作式多任务处理。纤程可以在执行过程中暂停(suspend),将控制权交还给调用者,然后在需要时恢复(resume)继续执行。与操作系统级别的线程不同,纤程由 PHP 用户代码管理调度,无需内核参与,因此创建和切换的开销极低。

PHP 版本

Fiber 从 PHP 8.1 开始可用。使用纤程需要 PHP 8.1 或更高版本。

基础概念

什么是纤程

Fiber 是一种可暂停和恢复的执行上下文。你可以将它理解为一个"可以暂停的函数"——它有自己的调用栈和局部变量,但在执行到某个点时可以主动让出控制权,稍后再继续。

Fiber vs 线程 vs 协程

特性Fiber(纤程)线程(Thread)协程(Coroutine)
调度方式协作式(用户态)抢占式(内核态)协作式(用户态)
内存开销极小较大极小
创建成本极低较高极低
PHP 内置8.1+ 原生不支持通过扩展(Swoole)
共享内存是(同一进程)是(同一进程)是(同一进程)
适用场景异步IO、并发任务CPU 密集并行高并发网络

Fiber 核心方法

  • new Fiber(callable $callback) — 创建纤程
  • Fiber::start() — 启动纤程
  • Fiber::suspend() — 在纤程内部暂停执行
  • Fiber::resume(mixed $value) — 恢复暂停的纤程
  • Fiber::throw(\Throwable $exception) — 向纤程抛出异常

FiberError

当在不正确的时机调用 Fiber 方法时(如在非暂停状态下恢复纤程),会抛出 FiberError

语法与代码

基本 Fiber 使用

php
<?php
declare(strict_types=1);

$fiber = new Fiber(function (): void {
    $value = Fiber::suspend('fiber started');
    echo "Fiber received: {$value}\n";
    Fiber::suspend('fiber resumed');
    echo "Fiber finished\n";
});

// 启动纤程
$result = $fiber->start();
echo "Main received: {$result}\n";
// Main received: fiber started

// 恢复纤程
$result = $fiber->resume('hello from main');
echo "Main received: {$result}\n";
// Fiber received: hello from main
// Main received: fiber resumed

// 再次恢复
$fiber->resume();
// Fiber finished

Fiber 的生命周期

php
<?php
declare(strict_types=1);

$fiber = new Fiber(function (): void {
    echo "Fiber: 开始执行\n";
    $data = Fiber::suspend('第一步完成');

    echo "Fiber: 收到 {$data}\n";
    Fiber::suspend('第二步完成');

    echo "Fiber: 完成\n";
});

echo "状态: " . $fiber->status() . "\n"; // 未启动

$fiber->start();
echo "状态: " . $fiber->status() . "\n"; // 暂停

$fiber->resume('数据');
echo "状态: " . $fiber->status() . "\n"; // 暂停

$fiber->resume();
echo "状态: " . $fiber->status() . "\n"; // 已完成

Fiber 的返回值

php
<?php
declare(strict_types=1);

$fiber = new Fiber(function (): int {
    Fiber::suspend();
    return 42;
});

$fiber->start();
$result = $fiber->resume();
echo "Fiber 返回值: {$result}\n"; // 42

Fiber 中的异常

php
<?php
declare(strict_types=1);

$fiber = new Fiber(function (): void {
    try {
        Fiber::suspend('暂停中');
    } catch (\RuntimeException $e) {
        echo "Fiber 捕获异常: {$e->getMessage()}\n";
        return;
    }
    echo "这行不会执行\n";
});

$fiber->start();
$fiber->throw(new \RuntimeException('外部抛出的异常'));
// Fiber 捕获异常: 外部抛出的异常

Fiber::start() 传入初始值

php
<?php
declare(strict_types=1);

$fiber = new Fiber(function (string $name): void {
    echo "Hello, {$name}!\n";
    Fiber::suspend('greeted');
    echo "Goodbye, {$name}!\n";
});

$fiber->start('Alice');
// Hello, Alice!

$fiber->resume();
// Goodbye, Alice!

详细说明

Fiber 状态

Fiber::status() 返回纤程的当前状态,可能的值包括:

状态说明
Fiber::STATUS_INIT纤程已创建但尚未启动
Fiber::STATUS_RUNNING纤程正在执行(仅在回调内部可见)
Fiber::STATUS_SUSPENDED纤程在 Fiber::suspend() 处暂停
Fiber::STATUS_FINISHED纤程已执行完毕
Fiber::STATUS_DEAD纤程因异常而终止

Fiber 的调用栈

每个 Fiber 有自己独立的调用栈。这意味着在 Fiber 内部的递归调用、局部变量等不会与主线程或其他 Fiber 的调用栈互相影响。

Fiber 的限制

  1. 不能跨 Fiber 共享连接:数据库连接、文件句柄等资源在 Fiber 之间切换时需要注意状态一致性。
  2. 不能嵌套启动:不能在一个 Fiber 内部启动另一个 Fiber(除非通过 suspend/resume 机制间接实现)。
  3. 不能在析构函数中使用:在析构函数中启动或恢复 Fiber 可能导致不可预测的行为。

实战示例

实战:模拟异步 IO

php
<?php
declare(strict_types=1);

class EventLoop
{
    /** @var \SplQueue<array{Fiber, mixed}> */
    private \SplQueue $readyQueue;

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

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

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

            try {
                if ($fiber->status() === \Fiber::STATUS_SUSPENDED) {
                    $fiber->resume($value);
                } elseif ($fiber->status() === \Fiber::STATUS_INIT) {
                    $fiber->start($value);
                }
            } catch (\Throwable $e) {
                echo "Fiber 异常: {$e->getMessage()}\n";
            }
        }
    }
}

$loop = new EventLoop();

$loop->schedule(new Fiber(function (): void {
    echo "任务 A: 步骤 1\n";
    Fiber::suspend();
    echo "任务 A: 步骤 2\n";
}));

$loop->schedule(new Fiber(function (): void {
    echo "任务 B: 步骤 1\n";
    Fiber::suspend();
    echo "任务 B: 步骤 2\n";
}));

$loop->run();
// 任务 A: 步骤 1
// 任务 B: 步骤 1
// 任务 A: 步骤 2
// 任务 B: 步骤 2

实战:模拟定时器

php
<?php
declare(strict_types=1);

class TimerLoop
{
    /** @var array<int, \Fiber> */
    private array $timers = [];
    private int $nextId = 1;
    private int $tick = 0;

    public function setTimeout(callable $callback, int $delay): int
    {
        $id = $this->nextId++;
        $this->timers[$id] = new \Fiber(function () use ($callback, $delay): void {
            Fiber::suspend($delay);
            $callback();
        });

        return $id;
    }

    public function tick(): void
    {
        foreach ($this->timers as $id => $fiber) {
            if ($fiber->status() === \Fiber::STATUS_INIT) {
                $delay = $fiber->start();
                echo "  定时器 {$id}: 设置延迟 {$delay}ms\n";
            } elseif ($fiber->status() === \Fiber::STATUS_SUSPENDED) {
                // 模拟延迟递减
                $this->tick++;
                if ($this->tick >= 3) {
                    $fiber->resume();
                    unset($this->timers[$id]);
                }
            } elseif ($fiber->status() === \Fiber::STATUS_FINISHED) {
                unset($this->timers[$id]);
            }
        }
    }

    public function hasPendingTimers(): bool
    {
        return !empty($this->timers);
    }
}

$loop = new TimerLoop();

$loop->setTimeout(function (): void {
    echo "定时器触发: 任务 A 完成\n";
}, 3000);

$loop->setTimeout(function (): void {
    echo "定时器触发: 任务 B 完成\n";
}, 2000);

while ($loop->hasPendingTimers()) {
    echo "Tick {$loop->tick}\n";
    $loop->tick();
}

注意事项

Fiber 不能用于并行计算

协作式调度

Fiber 是协作式调度的,不是并行执行。如果你需要真正的并行计算(利用多核 CPU),需要使用多进程(pcntl_fork)或外部进程管理。

Fiber 与现有代码的兼容性

大多数现有 PHP 代码可以在 Fiber 内部运行,但某些全局状态(如静态变量、单例)在 Fiber 间共享时需要特别注意。

内存泄漏风险

如果一个 Fiber 永远没有被恢复或抛出异常,它的调用栈和局部变量将不会被释放,可能导致内存泄漏。

最佳实践

  1. 确保每个 Fiber 最终完成:确保每个 Fiber 最终执行完毕或被抛出异常,避免内存泄漏。
  2. 使用事件循环模式:将 Fiber 与事件循环结合使用,实现结构化的异步编程。
  3. 避免在 Fiber 中使用全局状态:全局状态在 Fiber 间共享,容易导致竞态条件。
  4. 使用 try/finally 清理资源:在 Fiber 内部使用 try/finally 确保资源被正确释放。
  5. 考虑使用成熟的异步框架:如 ReactPHP、Amp 等已经基于 Fiber 提供了完善的异步生态。
php
<?php
declare(strict_types=1);

// 推荐模式:使用 try/finally
$fiber = new Fiber(function (): void {
    $resource = acquireResource();
    try {
        // 使用资源...
        Fiber::suspend();
    } finally {
        releaseResource($resource);
    }
});

Fiber 的技术原理

协作式多任务 vs 抢占式多任务

Fiber 实现的是协作式多任务(Cooperative Multitasking),与操作系统级别的抢占式多任务(Preemptive Multitasking)有本质区别:

特性协作式多任务(Fiber)抢占式多任务(线程/进程)
调度方式主动让出控制权操作系统强制切换
上下文切换开销极小(用户态)较大(内核态切换)
并发安全无需锁(单线程)需要锁/信号量
阻塞风险一个 Fiber 阻塞会阻塞所有每个线程独立
适用场景IO 密集型CPU 密集型
实现复杂度需要异步 API 配合相对简单

Fiber 与线程的对比

php
<?php
declare(strict_types=1);

// 线程模型(伪代码,PHP 不支持原生线程)
// Thread 1: readFile('a.txt')
// Thread 2: readFile('b.txt')
// OS 自动在两个线程间切换,不需要手动让出

// Fiber 模型
$fiber1 = new \Fiber(function (): void {
    $result = readfileAsync('a.txt');  // 需要异步 API
    // Fiber::suspend() 在异步 API 内部自动调用
    echo "文件 a.txt 内容: {$result}\n";
});

$fiber2 = new \Fiber(function (): void {
    $result = readfileAsync('b.txt');  // 需要异步 API
    echo "文件 b.txt 内容: {$result}\n";
});

$fiber1->start();
$fiber2->start();
// 事件循环驱动 Fiber 恢复

Fiber 与 Swoole Coroutine 的对比

Swoole 提供了 PHP 的协程实现,与 PHP 原生 Fiber 相比:

特性PHP FiberSwoole Coroutine
PHP 版本8.1+需要安装 Swoole 扩展
实现方式语言级别C 扩展
Hook 范围无自动 hook自动 hook 阻塞函数
API 兼容同步代码不自动变为异步同步代码可自动变为异步
生态系统纯 PHP 生态Swoole 生态
学习曲线较高(需理解 Fiber 机制)较低(近乎透明的异步)

选型建议

如果项目已经使用 Swoole,可以继续使用 Swoole 协程。如果追求纯 PHP 标准库方案,或构建自定义异步框架,PHP 原生 Fiber 是更好的选择。

Fiber 的使用场景

1. 异步 HTTP 客户端

php
<?php
declare(strict_types=1);

class AsyncHttpClient
{
    private \Fiber $fiber;

    public function get(string $url): string
    {
        $this->fiber = new \Fiber(function () use ($url): void {
            // 模拟异步 HTTP 请求
            $response = $this->fetchAsync($url);
            \Fiber::suspend($response);
        });

        $this->fiber->start();
        return $this->fiber->getReturn();
    }

    private function fetchAsync(string $url): string
    {
        // 实际实现需要事件循环集成
        return file_get_contents($url);
    }
}

2. 并发任务处理

php
<?php
declare(strict_types=1);

function processConcurrently(array $urls): array
{
    $fibers = [];
    $results = [];

    foreach ($urls as $index => $url) {
        $fibers[$index] = new \Fiber(function () use ($url, &$results, $index): void {
            // 模拟耗时操作
            usleep(rand(100000, 500000));
            $results[$index] = "完成: {$url}";
            \Fiber::suspend();
        });
        $fibers[$index]->start();
    }

    // 恢复所有 Fiber
    foreach ($fibers as $fiber) {
        if ($fiber->isSuspended()) {
            $fiber->resume();
        }
    }

    ksort($results);
    return $results;
}

常见误区与 FAQ

Fiber 可以并行执行吗?

不能。PHP Fiber 是单线程的协作式并发。同一时刻只有一个 Fiber 在执行。Fiber 提供的是并发(Concurrency),不是并行(Parallelism)。如果需要真正的并行计算,应使用 parallel 扩展或多进程。

Fiber 可以替代 async/await 吗?

概念上类似,但 PHP 原生没有 async/await 语法。Fiber 提供了实现 async/await 的底层原语。一些框架(如 ReactPHP、Amp)正在基于 Fiber 构建更高层的异步 API。

Fiber 中的异常如何处理?

Fiber 内部抛出且未被捕获的异常会通过 FiberError 传递给调用者。也可以使用 $fiber->throw() 向挂起的 Fiber 投掷异常。

参考链接