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 原生 Fiber | Swoole Fiber |
|---|---|---|
| 安装方式 | 内置(PHP 8.1+) | 扩展安装 |
| 异步 IO | 需自行实现或使用框架 | 内置(Socket、File、HttpClient) |
| 性能 | 用户态,纯 PHP | 内核态优化 |
| 生态 | ReactPHP/Amp/Revolt | Swoole 框架 |
| 协程调度 | 手动(用户代码) | 自动(框架调度) |
| 适用场景 | 轻量级异步 | 高性能服务器 |
选择建议
对于简单的异步需求,使用 PHP 原生 Fiber + ReactPHP/Amp。对于高性能 HTTP 服务器和协程调度,Swoole 仍然是更成熟的选择。
PHP 原生异步生态发展
PHP 8.1 Fiber 的引入推动了异步生态的变革:
- ReactPHP — 正在逐步采用 Fiber 简化异步 API
- Amp — 已完全基于 Fiber 重写,提供更自然的异步编程体验
- 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 之间的切换点和数据流动。
最佳实践
- 使用成熟的异步框架:除非有特殊需求,否则优先使用 ReactPHP、Amp 等成熟框架。
- 保持 Fiber 的独立性:每个 Fiber 应该尽可能独立,避免共享可变状态。
- 限制 Fiber 的生命周期:确保 Fiber 在合理的时间内完成,避免无限等待。
- 使用类型安全:为 Fiber 的 suspend/resume 传递值添加类型提示。
- 监控 Fiber 状态:在生产环境中监控 Fiber 的创建、暂停和完成情况。
php
<?php
declare(strict_types=1);
// 推荐模式:使用框架的异步 API
// 以 Amp 为例(伪代码)
// use Amp\ByteStream;
// $content = ByteStream\buffer(ByteStream\get('https://example.com'));
// echo $content;