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
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 finishedFiber 的生命周期
<?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
declare(strict_types=1);
$fiber = new Fiber(function (): int {
Fiber::suspend();
return 42;
});
$fiber->start();
$result = $fiber->resume();
echo "Fiber 返回值: {$result}\n"; // 42Fiber 中的异常
<?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
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 的限制
- 不能跨 Fiber 共享连接:数据库连接、文件句柄等资源在 Fiber 之间切换时需要注意状态一致性。
- 不能嵌套启动:不能在一个 Fiber 内部启动另一个 Fiber(除非通过 suspend/resume 机制间接实现)。
- 不能在析构函数中使用:在析构函数中启动或恢复 Fiber 可能导致不可预测的行为。
实战示例
实战:模拟异步 IO
<?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
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 永远没有被恢复或抛出异常,它的调用栈和局部变量将不会被释放,可能导致内存泄漏。
最佳实践
- 确保每个 Fiber 最终完成:确保每个 Fiber 最终执行完毕或被抛出异常,避免内存泄漏。
- 使用事件循环模式:将 Fiber 与事件循环结合使用,实现结构化的异步编程。
- 避免在 Fiber 中使用全局状态:全局状态在 Fiber 间共享,容易导致竞态条件。
- 使用 try/finally 清理资源:在 Fiber 内部使用 try/finally 确保资源被正确释放。
- 考虑使用成熟的异步框架:如 ReactPHP、Amp 等已经基于 Fiber 提供了完善的异步生态。
<?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
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 Fiber | Swoole Coroutine |
|---|---|---|
| PHP 版本 | 8.1+ | 需要安装 Swoole 扩展 |
| 实现方式 | 语言级别 | C 扩展 |
| Hook 范围 | 无自动 hook | 自动 hook 阻塞函数 |
| API 兼容 | 同步代码不自动变为异步 | 同步代码可自动变为异步 |
| 生态系统 | 纯 PHP 生态 | Swoole 生态 |
| 学习曲线 | 较高(需理解 Fiber 机制) | 较低(近乎透明的异步) |
选型建议
如果项目已经使用 Swoole,可以继续使用 Swoole 协程。如果追求纯 PHP 标准库方案,或构建自定义异步框架,PHP 原生 Fiber 是更好的选择。
Fiber 的使用场景
1. 异步 HTTP 客户端
<?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
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 投掷异常。