多年以来,PHP 开发者面对高并发场景时总需要借助 Swoole、ReactPHP 等第三方扩展或框架来实现异步编程。但随着 PHP 8.1 的发布,原生的 Fiber(纤程)终于走进了核心语言层。Fiber 并不是开箱即用的异步 I/O 方案,但它提供了一块关键的积木——协作式多任务的基础单元。掌握 Fiber 意味着你可以在纯 PHP 环境中构建自定义的、轻量级的协程调度器,从而深入理解异步编程的本质。本文将带你从零开始,用 Fiber 打造一个实用的任务调度器,并实现异步并发 HTTP 请求。
一、Fiber 是什么?为什么需要它?
Fiber 是一种可暂停、可恢复的执行单元。它允许你编写看起来像同步代码的代码,却能在内部主动让出执行权,让其他任务运行。与传统生成器(Generator)不同,Fiber 是专门为协程设计的,它拥有独立的调用栈,可以在任意深度暂停和恢复。
在 Fiber 出现之前,PHP 只能通过生成器模拟有限的协作式多任务,且无法在嵌套函数调用中暂停。Fiber 补全了这一缺失,使得开发者可以构建类似 JavaScript async/await 的并发模型,而无需依赖扩展。
核心概念:
- Fiber::suspend() —— 在 Fiber 内部调用,暂停当前纤程并返回一个值给外部。
- Fiber::resume() —— 在 Fiber 外部调用,恢复执行暂停的纤程,并传入一个值。
- Fiber::isSuspended() / isStarted() / isTerminated() —— 检查纤程状态。
二、基础语法与第一个 Fiber
让我们通过一个最简单的例子理解 Fiber 的执行流程:
$fiber = new Fiber(function (): void {
echo "Fiber 开始n";
$value = Fiber::suspend('暂停点1');
echo "收到外部传入的值: " . $value . "n";
$value2 = Fiber::suspend('暂停点2');
echo "再次收到: " . $value2 . "n";
});
echo "主程序启动n";
$result1 = $fiber->start(); // 启动 Fiber 直到遇到 suspend
echo "主程序收到: " . $result1 . "n";
$result2 = $fiber->resume('来自主程序的数据'); // 恢复并传入值
echo "主程序收到: " . $result2 . "n";
$fiber->resume('最后的数据');
echo "主程序结束n";
输出结果:
主程序启动
Fiber 开始
主程序收到: 暂停点1
收到外部传入的值: 来自主程序的数据
主程序收到: 暂停点2
再次收到: 最后的数据
主程序结束
观察执行顺序可以发现:主程序和 Fiber 交替运行,每次 Fiber 调用 suspend 时控制权回到主程序,主程序通过 resume 将控制权交还 Fiber。这正是协作式调度的基本机制。
三、构建一个简单的任务调度器
有了 Fiber,我们就可以实现一个轻量级的协程调度器。调度器负责维护一个任务队列,循环遍历并恢复各个 Fiber,当某个 Fiber 暂停(例如等待 I/O 或睡眠)时,调度器将其暂时移出活跃队列,直到等待条件满足再重新加入。
首先定义任务结构:
class Task {
public Fiber $fiber;
public mixed $result = null;
public bool $finished = false;
public function __construct(callable $callable) {
$this->fiber = new Fiber(function () use ($callable) {
return $callable($this);
});
}
public function run(): void {
if ($this->fiber->isSuspended()) {
$this->fiber->resume();
} else {
$this->fiber->start();
}
}
}
调度器核心逻辑:
class Scheduler {
private array $tasks = [];
private array $sleepingTasks = []; // 存储睡眠任务及其唤醒时间
public function addTask(callable $callable): void {
$this->tasks[] = new Task($callable);
}
public function run(): void {
while (!empty($this->tasks) || !empty($this->sleepingTasks)) {
// 处理可运行的任务
foreach ($this->tasks as $key => $task) {
if ($task->finished) {
unset($this->tasks[$key]);
continue;
}
$task->run();
}
// 检查睡眠任务是否可以唤醒
$now = microtime(true);
foreach ($this->sleepingTasks as $wakeTime => $sleepingGroup) {
if ($wakeTime tasks[] = $task;
}
unset($this->sleepingTasks[$wakeTime]);
}
}
// 如果没有任务可运行但仍有睡眠任务,则短暂休眠避免CPU空转
if (empty($this->tasks) && !empty($this->sleepingTasks)) {
usleep(10000); // 10ms
}
}
}
public function suspendTask(Task $task, mixed $value = null): void {
if ($task->fiber->isStarted() && !$task->fiber->isTerminated()) {
$task->fiber->suspend($value);
}
}
// 让任务睡眠指定秒数
public function sleep(float $seconds): void {
$task = $this->currentTask ?? null;
if ($task) {
$wakeTime = microtime(true) + $seconds;
$this->sleepingTasks[(string)$wakeTime][] = $task;
// 从活跃任务中移除(会在调度循环中自动移除,这里标记即可)
foreach ($this->tasks as $key => $t) {
if ($t === $task) {
unset($this->tasks[$key]);
break;
}
}
$this->suspendTask($task);
}
}
}
为了让任务内部能够调用 sleep,我们需要一个全局上下文或通过 Task 传递调度器引用。这里简化设计:在 Task 中添加调度器属性,并在 sleep 时调用。
更新后的 Task 类:
class Task {
public Fiber $fiber;
public ?Scheduler $scheduler = null;
public bool $finished = false;
public function __construct(callable $callable) {
$this->fiber = new Fiber(function () use ($callable) {
return $callable($this);
});
}
// ...
}
然后提供辅助函数:
function asyncSleep(Task $task, float $seconds): void {
$task->scheduler->sleep($seconds);
}
四、实战:异步并发 HTTP 请求
现在我们用 Fiber 模拟异步 HTTP 请求。假设我们有一个“阻塞”的 HTTP 客户端(实际上用 file_get_contents 或 curl),但我们希望在等待网络响应时不阻塞其他协程。由于 Fiber 本身不提供非阻塞 I/O,我们需要用生成器模式将 I/O 操作注册到调度器的事件循环中。这里为了可运行性,我们使用 stream_socket_client 配合 stream_select 来实现非阻塞 HTTP 请求,或者简单模拟网络延迟。
为简洁起见,我们先用 usleep 模拟 I/O 等待,并通过调度器的 sleep 实现并发效果:
$scheduler = new Scheduler();
// 模拟一个异步HTTP请求任务
$scheduler->addTask(function (Task $task) use ($scheduler) {
echo "任务1:开始请求 API-An";
// 模拟耗时操作:睡眠1秒
asyncSleep($task, 1.0);
echo "任务1:收到 API-A 响应n";
});
$scheduler->addTask(function (Task $task) use ($scheduler) {
echo "任务2:开始请求 API-Bn";
asyncSleep($task, 0.5);
echo "任务2:收到 API-B 响应n";
});
$scheduler->addTask(function (Task $task) use ($scheduler) {
echo "任务3:执行计算n";
$sum = 0;
for ($i = 0; $i run();
$elapsed = microtime(true) - $start;
echo "总耗时: {$elapsed} 秒n";
输出类似:
调度器开始运行
任务1:开始请求 API-A
任务2:开始请求 API-B
任务3:执行计算
任务3:计算完成,结果 = 499999500000
任务2:收到 API-B 响应
任务1:收到 API-A 响应
总耗时: 1.001 秒
注意,整个调度器总耗时约 1 秒,而不是 1.5 秒,因为任务1和任务2的睡眠是并发的。尽管我们使用的是阻塞的 sleep 模拟,但调度器在任务睡眠时将其移出活跃队列,并允许其他任务运行,从而实现了并发效果。
五、集成真实非阻塞 I/O:使用 stream_select
要真正发挥 Fiber 的威力,需要将调度器与系统级别的非阻塞 I/O 结合。以下演示如何用 stream_select 封装一个非阻塞的 HTTP 请求函数,让调度器在等待数据时不会阻塞整个进程。
function asyncHttpGet(Task $task, string $url): string {
$parts = parse_url($url);
$host = $parts['host'];
$port = $parts['port'] ?? 80;
$path = $parts['path'] ?? '/';
$fp = stream_socket_client("tcp://$host:$port", $errno, $errstr, 10, STREAM_CLIENT_ASYNC_CONNECT);
stream_set_blocking($fp, false);
$request = "GET $path HTTP/1.0rnHost: $hostrnConnection: closernrn";
fwrite($fp, $request);
$response = '';
$read = [$fp];
$write = null;
$except = null;
// 等待可读,同时将控制权交还给调度器
while (!feof($fp)) {
$task->scheduler->waitForReadable($fp); // 自定义方法,挂起当前任务
$data = fread($fp, 8192);
$response .= $data;
}
fclose($fp);
return $response;
}
调度器需要增加 I/O 等待队列和 stream_select 事件循环:
class Scheduler {
// ... 其他代码
private array $readWaiters = []; // 存储等待读取的stream和对应任务
public function waitForReadable($stream): void {
$task = $this->currentTask;
$this->readWaiters[(int)$stream] = $task;
$this->suspendTask($task);
}
public function run(): void {
while (!empty($this->tasks) || !empty($this->sleepingTasks) || !empty($this->readWaiters)) {
// ... 运行任务 ...
// 处理 I/O 等待
if (!empty($this->readWaiters)) {
$readStreams = array_keys($this->readWaiters);
$write = null;
$except = null;
if (stream_select($readStreams, $write, $except, 0, 50000) > 0) {
foreach ($readStreams as $stream) {
$task = $this->readWaiters[(int)$stream];
unset($this->readWaiters[(int)$stream]);
$this->tasks[] = $task; // 重新加入活跃队列
}
}
}
// ...
}
}
}
这样,我们就实现了一个基于 Fiber 的完整协程调度器,可以在等待网络 I/O 时让出 CPU,实现真正意义上的并发处理。对于大量 I/O 密集型场景(如爬虫、API 网关),这种模式可以极大提升吞吐量。
六、性能对比与适用场景
我们使用一段简单的基准测试来对比传统同步请求和基于 Fiber 的并发请求。假设每个请求耗时 100ms,执行 10 个请求:
- 传统同步方式:总耗时 10 × 100ms = 1 秒。
- Fiber 调度器 + 非阻塞 I/O:总耗时接近 100ms(取决于最慢的单个请求)。
在真实的网络环境中,性能提升更加显著。此外,Fiber 的内存开销极小(每个纤程约 2KB),一台机器可以轻松运行数万个并发纤程,远优于多进程或多线程模型。
适用场景:
- 高并发 HTTP 客户端(爬虫、数据聚合)
- WebSocket 服务端(配合自定义事件循环)
- 数据库连接池与异步查询调度
- API 网关和微服务间的并发调用编排
七、注意事项与最佳实践
- Fiber 只是构建块,不是完整方案。 你需要自己实现调度器或使用已有的库(如 revolt/event-loop)。
- 避免在 Fiber 内抛出未捕获的异常。 如果 Fiber 内部异常未被 try-catch 处理,会传播到外部,导致调度器中断。应在任务中妥善处理异常。
- 不要阻塞整个进程。 即使在 Fiber 中,调用
sleep()等阻塞函数仍会冻结整个进程。必须使用调度器提供的非阻塞等价操作。 - 注意状态共享与并发安全。 由于协程是单线程并发,不存在真正的同时执行,但仍需小心全局状态的修改,特别是在 suspend 点前后。
- 逐步迁移,渐进增强。 可以在现有项目中逐步将 I/O 密集型部分用 Fiber 包装,而不必重写整个应用。
八、总结
PHP 8.1 的 Fiber 为 PHP 异步编程打开了新的大门。它虽然不像 Swoole 那样提供完整的协程生态,但它作为语言级原语,让开发者能够以更轻量、更可控的方式实现协作式多任务。通过本文的自建调度器,你应该已经理解了 Fiber 的工作原理以及如何将其应用于实际的并发场景。建议读者从简单的睡眠协程开始,再逐步集成非阻塞 I/O,最终构建出适合自己业务的高性能 PHP 应用。
Fiber 的出现标志着 PHP 在并发领域迈出了重要一步,未来随着生态的完善,纯 PHP 协程编程必将成为主流。现在,就是学习和掌握它的最佳时机。

