Skip to content

Fiber 状态管理

概述

Fiber 在其生命周期中会经历多种状态转换。理解这些状态及其转换规则对于正确使用 Fiber 至关重要。本章将详细讲解 Fiber 的状态枚举、状态转换图、生命周期管理,以及在每个状态下哪些操作是允许的。

关键概念

Fiber 的状态决定了你可以对其执行的操作。在错误的状态下调用 Fiber 方法会抛出 FiberError

基础概念

Fiber 状态枚举

PHP 定义了以下 Fiber 状态常量:

  • Fiber::STATUS_INIT — 纤程已创建但尚未启动
  • Fiber::STATUS_RUNNING — 纤程正在执行
  • Fiber::STATUS_SUSPENDED — 纤程已暂停
  • Fiber::STATUS_FINISHED — 纤程已正常结束
  • Fiber::STATUS_DEAD — 纤程因异常终止

状态转换规则

每个状态只允许特定的操作,执行其他操作会导致 FiberError

Fiber::getCurrent()

Fiber::getCurrent() 返回当前正在执行的 Fiber 实例。如果在 Fiber 外部调用,返回 null

语法与代码

获取 Fiber 状态

php
<?php
declare(strict_types=1);

$fiber = new Fiber(function (): void {
    echo "内部状态: " . Fiber::getCurrent()->status() . "\n";
    Fiber::suspend();
    echo "恢复后继续\n";
});

echo "初始状态: " . $fiber->status() . "\n"; // INIT (1)

$fiber->start();
// 内部状态: RUNNING (2)

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

$fiber->resume();
echo "结束状态: " . $fiber->status() . "\n"; // FINISHED (4)

各状态的允许操作

php
<?php
declare(strict_types=1);

function testStateTransitions(): void
{
    $fiber = new Fiber(function (): void {
        Fiber::suspend('paused');
        return 'done';
    });

    // --- INIT 状态 ---
    assert($fiber->status() === \Fiber::STATUS_INIT);
    // start() ✓  throw() ✗  resume() ✗
    $result = $fiber->start();

    // --- SUSPENDED 状态 ---
    assert($fiber->status() === \Fiber::STATUS_SUSPENDED);
    // start() ✗  throw() ✓  resume() ✓
    $result = $fiber->resume();

    // --- FINISHED 状态 ---
    assert($fiber->status() === \Fiber::STATUS_FINISHED);
    // start() ✗  throw() ✗  resume() ✗
}

testStateTransitions();

Fiber 异常终止(DEAD 状态)

php
<?php
declare(strict_types=1);

$fiber = new Fiber(function (): void {
    Fiber::suspend('alive');
    throw new \RuntimeException('fatal error');
});

$fiber->start();

try {
    $fiber->resume();
} catch (\RuntimeException $e) {
    echo "异常: {$e->getMessage()}\n";
}

echo "状态: " . $fiber->status() . "\n"; // FINISHED (4) — 即使异常也标记为 FINISHED

状态说明

PHP 8.1 中 Fiber::STATUS_DEAD 在实际使用中很少出现——因异常终止的 Fiber 状态实际上仍为 FINISHEDDEAD 状态可能在内部实现中用于标记其他终止场景。

Fiber::getCurrent() 在不同上下文中

php
<?php
declare(strict_types=1);

echo "主线程: " . var_export(\Fiber::getCurrent(), true) . "\n";
// 主线程: NULL

$fiber = new Fiber(function (): void {
    $current = \Fiber::getCurrent();
    echo "Fiber 内部: " . var_export($current !== null, true) . "\n";
    Fiber::suspend();
});

$fiber->start();
// Fiber 内部: true

详细说明

完整状态转换图

              new Fiber()


            ┌──────────┐
            │   INIT   │
            └────┬─────┘
                 │ start()

            ┌──────────┐
            │ RUNNING  │◄───────┐
            └──┬──┬──┬──┘        │
               │  │  │           │
               │  │  └── suspend() ──►┌───────────┐
               │  │                  │ SUSPENDED │
               │  │                  └─────┬─────┘
               │  │                   resume()/throw()
               │  │                        │
               │  └── 异常 ──────────────►│
               │                           │
               ▼                           │
        ┌───────────┐◄─────────────────────┘
        │ FINISHED  │
        └───────────┘

状态与方法的对照表

方法INITRUNNINGSUSPENDEDFINISHED
start()允许FiberErrorFiberErrorFiberError
resume()FiberErrorFiberError允许FiberError
throw()FiberErrorFiberError允许FiberError
suspend()-允许(内部)--
status()允许允许允许允许
getReturn()FiberErrorFiberErrorFiberError允许

getReturn() 方法

php
<?php
declare(strict_types=1);

$fiber = new Fiber(function (): string {
    Fiber::suspend();
    return 'final result';
});

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

echo $fiber->getReturn() . "\n"; // final result

// 对于 void 类型的 Fiber
$fiber2 = new Fiber(function (): void {
    Fiber::suspend();
});
$fiber2->start();
$fiber2->resume();
echo var_export($fiber2->getReturn(), true); // NULL

实战示例

实战:状态监控器

php
<?php
declare(strict_types=1);

class FiberMonitor
{
    /** @var array<string, string> */
    private array $history = [];

    public function track(string $name, \Fiber $fiber): void
    {
        $status = match ($fiber->status()) {
            \Fiber::STATUS_INIT => 'INIT',
            \Fiber::STATUS_RUNNING => 'RUNNING',
            \Fiber::STATUS_SUSPENDED => 'SUSPENDED',
            \Fiber::STATUS_FINISHED => 'FINISHED',
            default => 'UNKNOWN',
        };
        $this->history[$name][] = [
            'status' => $status,
            'time' => microtime(true),
        ];
    }

    public function getHistory(string $name): array
    {
        return $this->history[$name] ?? [];
    }
}

$monitor = new FiberMonitor();
$fiber = new Fiber(function (): void {
    Fiber::suspend();
    Fiber::suspend();
});

$name = 'worker-1';
$monitor->track($name, $fiber); // INIT
$fiber->start();
$monitor->track($name, $fiber); // SUSPENDED
$fiber->resume();
$monitor->track($name, $fiber); // SUSPENDED
$fiber->resume();
$monitor->track($name, $fiber); // FINISHED

print_r($monitor->getHistory($name));

实战:状态驱动的任务系统

php
<?php
declare(strict_types=1);

class TaskSystem
{
    /** @var array<int, \Fiber> */
    private array $tasks = [];
    private int $taskId = 0;

    public function createTask(callable $callback): int
    {
        $id = $this->taskId++;
        $this->tasks[$id] = new \Fiber($callback);
        return $id;
    }

    public function tick(): void
    {
        foreach ($this->tasks as $id => $fiber) {
            $status = $fiber->status();

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

            if ($fiber->status() === \Fiber::STATUS_FINISHED) {
                unset($this->tasks[$id]);
            }
        }
    }

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

$system = new TaskSystem();
$system->createTask(function (): void {
    echo "任务 1: 开始\n";
    Fiber::suspend();
    echo "任务 1: 继续\n";
});

$system->createTask(function (): void {
    echo "任务 2: 开始\n";
    Fiber::suspend();
    Fiber::suspend();
    echo "任务 2: 结束\n";
});

while ($system->hasPendingTasks()) {
    $system->tick();
}

注意事项

不要在 RUNNING 状态操作 Fiber

在 Fiber 的回调函数内部,不能对自身调用 start()resume()throw()。这会导致死锁或 FiberError

状态检查的重要性

在调用 Fiber 的方法之前,最好先检查其状态,避免抛出 FiberError

php
<?php
declare(strict_types=1);

function safeResume(\Fiber $fiber, mixed $value = null): void
{
    if ($fiber->status() === \Fiber::STATUS_SUSPENDED) {
        $fiber->resume($value);
    }
}

getReturn 在未完成时调用

php
<?php
declare(strict_types=1);

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

$fiber->start();

try {
    $fiber->getReturn();
} catch (\Throwable $e) {
    echo "异常: " . $e->getMessage() . "\n";
    // The fiber has not returned
}

最佳实践

  1. 调用前检查状态:在操作 Fiber 之前使用 status() 检查当前状态。
  2. 使用 match 表达式处理状态:PHP 8.0+ 的 match 表达式非常适合处理 Fiber 状态。
  3. 封装安全的操作方法:创建封装类来处理 Fiber 状态检查和方法调用。
  4. 记录状态变化:在调试时记录 Fiber 的状态变化有助于排查问题。
php
<?php
declare(strict_types=1);

class SafeFiber
{
    public function __construct(
        private readonly \Fiber $fiber
    ) {}

    public function start(mixed ...$args): mixed
    {
        if ($this->fiber->status() !== \Fiber::STATUS_INIT) {
            throw new \RuntimeException('Fiber 不是 INIT 状态');
        }
        return $this->fiber->start(...$args);
    }

    public function resume(mixed $value = null): mixed
    {
        if ($this->fiber->status() !== \Fiber::STATUS_SUSPENDED) {
            throw new \RuntimeException('Fiber 不是 SUSPENDED 状态');
        }
        return $this->fiber->resume($value);
    }

    public function isRunning(): bool
    {
        return $this->fiber->status() === \Fiber::STATUS_SUSPENDED
            || $this->fiber->status() === \Fiber::STATUS_INIT;
    }
}

状态转换的详细分析

完整状态转换表

以下表格展示了每个方法在不同状态下的行为:

当前状态start()resume()throw()
未启动创建新栈,执行回调FiberErrorFiberError
挂起FiberError恢复执行,传入值恢复执行,抛出异常
已结束FiberErrorFiberErrorFiberError
已终止FiberErrorFiberErrorFiberError

Fiber 的错误处理

在 Fiber 执行过程中,如果发生未捕获的异常,Fiber 会进入终止(terminated)状态,异常会被转换为 FiberError 抛出给调用者。

php
<?php
declare(strict_types=1);

$fiber = new \Fiber(function (): void {
    throw new \RuntimeException("Fiber 内部错误");
});

try {
    $fiber->start();
} catch (\FiberError $e) {
    echo "捕获 FiberError: " . $e->getMessage() . "\n";
    $previous = $e->getPrevious();
    if ($previous !== null) {
        echo "原始异常: " . $previous->getMessage() . "\n";
    }
}

echo "Fiber 状态: " . match (true) {
    $fiber->isTerminated() => 'terminated',
    $fiber->isSuspended() => 'suspended',
    $fiber->isRunning() => 'running',
    default => 'unknown',
} . "\n";

FiberError vs 内部异常

FiberError 是 Fiber 状态操作错误的包装器,原始异常存储在 getPrevious() 中。务必检查 getPrevious() 获取真正的错误信息。

Fiber 栈的内存管理

每个 Fiber 创建时都会分配独立的调用栈。栈的大小可以通过 fiber.stack_size PHP INI 配置调整。

php
<?php
declare(strict_types=1);

// 查看当前 Fiber 栈大小配置
echo "Fiber 栈大小: " . ini_get('fiber.stack_size') . " bytes\n";
// 默认通常是 8MB 或由系统决定

栈大小调整

对于需要深度递归的 Fiber,可以适当增大栈大小。但需要注意,每个 Fiber 都会消耗对应大小的虚拟内存。

实战:Fiber 状态监控器

php
<?php
declare(strict_types=1);

class FiberMonitor
{
    /** @var array<string, \Fiber> */
    private array $fibers = [];

    /** @var array<string, string> */
    private array $states = [];

    public function create(string $name, callable $callback): \Fiber
    {
        $fiber = new \Fiber(function () use ($callback, $name): void {
            $this->states[$name] = 'running';
            $callback();
            $this->states[$name] = 'finished';
        });

        $this->fibers[$name] = $fiber;
        $this->states[$name] = 'created';
        return $fiber;
    }

    public function getStates(): array
    {
        foreach ($this->fibers as $name => $fiber) {
            if (isset($this->states[$name]) && $this->states[$name] !== 'finished') {
                $this->states[$name] = match (true) {
                    $fiber->isRunning() => 'running',
                    $fiber->isSuspended() => 'suspended',
                    $fiber->isTerminated() => 'terminated',
                    default => 'unknown',
                };
            }
        }
        return $this->states;
    }

    public function summary(): string
    {
        $lines = [];
        foreach ($this->getStates() as $name => $state) {
            $lines[] = "Fiber '{$name}': {$state}";
        }
        return implode("\n", $lines);
    }
}

$monitor = new FiberMonitor();
$fiber = $monitor->create('worker', function (): void {
    echo "工作中...\n";
    \Fiber::suspend();
    echo "恢复工作...\n";
});

$monitor->create('helper', function (): void {
    echo "辅助任务...\n";
});

echo $monitor->summary() . "\n";
$fiber->start();
echo $monitor->summary() . "\n";
$fiber->resume();
echo $monitor->summary() . "\n";

常见误区与 FAQ

Fiber 可以重启吗?

不能。一个 Fiber 一旦结束(正常结束或异常终止),就不能再次启动。必须创建新的 Fiber 实例。

如何判断 Fiber 是否可以 resume?

使用 $fiber->isSuspended() 方法。只有处于挂起状态的 Fiber 才可以被 resume()throw()

多个 Fiber 之间的通信

Fiber 之间通过 resume($value) 传递值进行通信。suspend() 的返回值就是 resume() 的参数,反之亦然。

php
<?php
declare(strict_types=1);

$producer = new \Fiber(function (): void {
    $data = ['task1', 'task2', 'task3'];
    foreach ($data as $item) {
        $consumerResponse = \Fiber::suspend($item);
        echo "生产者收到: {$consumerResponse}\n";
    }
});

$consumer = new \Fiber(function () use ($producer): void {
    while ($producer->isSuspended()) {
        $item = $producer->resume();
        echo "消费者处理: {$item}\n";
    }
});

$consumer->start();

参考链接