Fibers (协程) 是 PHP 8.1 引入的一个重要新特性,它为 PHP 带来了原生的用户空间并发能力。与传统的线程或进程不同,Fibers 允许代码在执行过程中暂停和恢复,而无需使用生成器 (Generators) 或复杂的事件循环回调。这使得开发者能够编写更具可读性和可维护性的异步非阻塞代码,从而更好地应对 I/O 密集型任务,如网络请求、数据库查询等。

核心思想:Fibers 是一种轻量级的并发原语,允许 PHP 代码在用户空间中实现非阻塞操作,通过显式地暂停和恢复执行,简化了异步代码的编写。


一、为什么需要 Fibers?

在 PHP 8.1 之前,实现异步非阻塞代码通常依赖于以下两种方式:

  1. Callbacks (回调函数)
    • 优点:简单直接,适用于简单的异步操作。
    • 缺点:容易陷入“回调地狱 (Callback Hell)”,代码可读性和维护性差,错误处理复杂。
  2. Generators (生成器)
    • 优点:通过 yield 实现了伪协程,可以在一定程度上改善回调地狱,允许代码暂停和恢复。
    • 缺点:生成器本质上是迭代器,其语义更偏向于数据生成。将生成器用于协程上下文切换,需要在外部有一个调度器 (Scheduler) 明确地 send()next(),实现起来较为复杂和不直观。

这两种方式都使得编写复杂的异步逻辑变得困难和笨拙。Fibers 的引入正是为了解决这些问题,提供一种更直观、更原生的方式来管理并发执行流。

Fibers 的核心优势在于:

  • 代码可读性:允许以同步的方式编写异步代码,避免了深层嵌套的回调。
  • 简化复杂异步逻辑:不再需要依赖生成器或复杂的外部调度逻辑进行上下文切换。
  • 用户空间轻量级:比操作系统线程/进程更轻量,上下文切换开销更小。
  • 不涉及并行:Fibers 本身不是并行执行,它们是协作式的多任务。在任何给定时间点,只有一个 Fiber 在运行,其他 Fiber 暂停等待恢复。

二、Fibers 的基本概念

2.1 什么是 Fiber?

Fiber 可以被视为一个轻量级的,用户空间级别的执行上下文。它拥有自己的栈和程序计数器,可以在任何时候被暂停,并在后续的任何时间点恢复。当一个 Fiber 被暂停时,其父 Fiber(或主执行流)将恢复执行。当一个 Fiber 被恢复时,它会从上次暂停的地方继续执行。

2.2 Fiber 的状态

一个 Fiber 可以处于以下几种状态:

  • Fiber::STATUS_INIT: 初始状态,尚未启动。
  • Fiber::STATUS_RUNNING: 正在执行。
  • Fiber::STATUS_SUSPENDED: 已暂停,等待恢复。
  • Fiber::STATUS_TERMINATED: 已终止(完成执行或抛出异常)。

2.3 Fiber 类的方法

PHP 提供了一个内置的 Fiber 类来创建和管理协程。

  • __construct(callable $callback)
    • 创建一个新的 Fiber 实例。$callback 是一个可调用对象,代表 Fiber 要执行的代码。
    • Fiber 创建后处于 Fiber::STATUS_INIT 状态,不会立即执行。
  • start(mixed ...$args): mixed
    • 启动 Fiber 的执行。Fiber 会从 $callback 的开头开始执行。
    • $args 会作为参数传递给 $callback
    • 如果 Fiber 在执行过程中通过 Fiber::suspend() 暂停,start() 方法会返回 Fiber::suspend() 传递的值。
    • 如果 Fiber 完成执行,start() 方法会返回 $callback 的返回值。
    • 如果 Fiber 抛出异常,start() 方法会抛出该异常。
  • suspend(mixed $value = null): mixed
    • 暂停当前正在执行的 Fiber,并将 $value 返回给恢复它的 Fiber(即调用 start()resume() 的 Fiber)。
    • 当该 Fiber 再次被恢复时,suspend() 方法会返回 resume() 传递的值。
  • resume(mixed $value = null): mixed
    • 恢复一个已暂停的 Fiber。
    • $value 会作为 Fiber::suspend() 的返回值传递给被恢复的 Fiber。
    • resume() 方法会返回被恢复 Fiber 中下一个 Fiber::suspend() 传递的值,或者如果 Fiber 完成执行,则返回其最终返回值。
  • throw(Throwable $exception): mixed
    • 在暂停的 Fiber 中抛出一个异常,并恢复其执行。
    • 该异常会被被恢复的 Fiber 内部捕获(如果存在 try...catch),或者导致 Fiber 终止。
  • isStarted(): bool
    • 检查 Fiber 是否已启动。
  • isSuspended(): bool
    • 检查 Fiber 是否已暂停。
  • isTerminated(): bool
    • 检查 Fiber 是否已终止。
  • getCurrent(): ?Fiber
    • 获取当前正在执行的 Fiber 实例。如果当前不在 Fiber 中,则返回 null
  • getReturn(): mixed
    • 获取已终止 Fiber 的返回值。

三、Fibers 示例

3.1 简单示例:暂停与恢复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

$fiber = new Fiber(function (): void {
echo "Fiber started.\n";
$value = Fiber::suspend('Hello from Fiber!'); // 暂停并返回 'Hello from Fiber!'
echo "Fiber resumed with: " . $value . "\n"; // 从这里继续执行
Fiber::suspend('Another suspend!');
echo "Fiber ending.\n";
return 'Fiber finished!';
});

// 启动 Fiber
$res1 = $fiber->start();
echo "Main context received: " . $res1 . "\n"; // 输出: Main context received: Hello from Fiber!

// 恢复 Fiber
$res2 = $fiber->resume('Resumed by Main!');
echo "Main context received again: " . $res2 . "\n"; // 输出: Main context received again: Another suspend!

// 再次恢复 Fiber,并传递最终结果
$finalResult = $fiber->resume('Final resume!');
echo "Main context received final: " . $finalResult . "\n"; // 输出: Main context received final: Fiber finished!

echo "Fiber status: " . ($fiber->isTerminated() ? "Terminated" : "Running") . "\n";
?>

输出:

1
2
3
4
5
6
7
Fiber started.
Main context received: Hello from Fiber!
Fiber resumed with: Resumed by Main!
Main context received again: Another suspend!
Fiber ending.
Main context received final: Fiber finished!
Fiber status: Terminated

3.2 模拟非阻塞 I/O

Fibers 的真正威力在于与事件循环 (Event Loop) 结合,模拟非阻塞 I/O。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sequenceDiagram
participant Main as 主程序
participant FiberA as Fiber A
participant FiberB as Fiber B
participant Network as 网络IO

Main->>FiberA: $fiberA->start()
FiberA->>Network: 发起网络请求 (比如 HTTP GET)
FiberA->>Main: Fiber::suspend() (等待网络响应)
Main->>FiberB: $fiberB->start()
FiberB->>Network: 发起另一个网络请求
FiberB->>Main: Fiber::suspend() (等待网络响应)
Main->>Main: 循环检查就绪的IO (Event Loop)
Network-->>Main: Fiber A 的网络响应到达
Main->>FiberA: $fiberA->resume(responseA)
FiberA->>FiberA: 处理响应A
FiberA->>Main: Fiber::suspend() 或 返回结果 (如果完成)
Network-->>Main: Fiber B 的网络响应到达
Main->>FiberB: $fiberB->resume(responseB)
FiberB->>FiberB: 处理响应B
FiberB->>Main: Fiber::suspend() 或 返回结果 (如果完成)
Main->>Main: 继续调度其他Fiber 或 处理其他任务

这是一个简化的例子,展示了如何使用 Fibers 模拟两个并发的“耗时操作”(实际上我们使用 sleep() 来模拟 I/O 延迟)。一个真正的异步框架会有一个事件循环来调度这些 Fibers。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<?php

// 模拟一个异步 I/O 操作 (例如网络请求、文件读写)
function asyncOperation(string $name, int $delaySeconds): string
{
echo "[Fiber $name] Starting operation, will take {$delaySeconds} seconds...\n";
// 暂停当前 Fiber,将控制权交还给调度器/主程序
// 实际异步框架中,这里会注册一个回调,当I/O完成后唤醒当前Fiber
Fiber::suspend();
echo "[Fiber $name] Operation resumed after {$delaySeconds} seconds.\n";
return "[Fiber $name] Result after {$delaySeconds} seconds.";
}

// 定义一个简单的调度器 (Event Loop 的简化版)
class Scheduler
{
private SplQueue $queue; // 存储待运行的 Fiber
private array $waitingFibers = []; // 存储因I/O暂停的 Fiber

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

public function addFiber(Fiber $fiber): void
{
$this->queue->enqueue($fiber);
$this->waitingFibers[spl_object_id($fiber)] = [
'fiber' => $fiber,
'start_time' => microtime(true),
'delay' => 0, // 初始未知延迟,实际中会从任务获取
];
}

public function run(): void
{
while (!$this->queue->isEmpty() || !empty($this->waitingFibers)) {
// 先处理队列中的 Fiber (模拟CPU密集型任务或已就绪的I/O)
while (!$this->queue->isEmpty()) {
$fiber = $this->queue->dequeue();
if ($fiber->isTerminated()) {
unset($this->waitingFibers[spl_object_id($fiber)]);
continue;
}

try {
if (!$fiber->isStarted()) {
$value = $fiber->start();
} else {
$value = $fiber->resume();
}

if ($fiber->isSuspended()) {
// Fiber 暂停了,等待 I/O,重新放回等待池
// 这里简化处理,实际需要注册到真正的 I/O 多路复用器
// 我们模拟一个延迟后唤醒
$this->waitingFibers[spl_object_id($fiber)]['start_time'] = microtime(true); // 重置等待时间
// 假设 asyncOperation 函数会告诉调度器它需要等待多久
// 这里我们手动设置一个延迟,方便演示
if (str_contains($value, 'delay')) {
preg_match('/delay:(\d+)/', $value, $matches);
if (isset($matches[1])) {
$this->waitingFibers[spl_object_id($fiber)]['delay'] = (int)$matches[1];
}
} else {
$this->waitingFibers[spl_object_id($fiber)]['delay'] = 2; // 默认2秒
}

} elseif ($fiber->isTerminated()) {
echo "Fiber finished with return: " . $fiber->getReturn() . "\n";
unset($this->waitingFibers[spl_object_id($fiber)]);
}
} catch (Throwable $e) {
echo "Fiber threw an exception: " . $e->getMessage() . "\n";
unset($this->waitingFibers[spl_object_id($fiber)]);
}
}

// 检查是否有等待中的 Fiber 已满足条件 (模拟 I/O 完成)
foreach ($this->waitingFibers as $id => &$waitingFiber) {
if ($waitingFiber['fiber']->isSuspended() && (microtime(true) - $waitingFiber['start_time']) >= $waitingFiber['delay']) {
echo "[Scheduler] Resuming Fiber {$id} after simulated I/O delay...\n";
$this->queue->enqueue($waitingFiber['fiber']); // 重新加入队列
// 模拟 I/O 结果
$waitingFiber['fiber']->resume("Simulated I/O result for Fiber {$id}");
unset($this->waitingFibers[$id]); // 从等待池移除
}
}

// 如果没有Fiber可运行且有等待中的Fiber,则等待一小段时间
if ($this->queue->isEmpty() && !empty($this->waitingFibers)) {
usleep(100000); // 等待100ms,模拟等待I/O事件
}
}
echo "Scheduler finished.\n";
}
}

$scheduler = new Scheduler();

// 创建第一个 Fiber
$fiber1 = new Fiber(function (string $name, int $delay) {
echo "Fiber {$name} started.\n";
// 实际中,这里会是 `async_read_file($path)->await()` 或 `http_client->get($url)->await()`
$result = asyncOperation($name, $delay); // 模拟耗时操作,Fiber::suspend()
echo "[Fiber {$name}] Received result: {$result}\n";
return "{$name} completed.";
});
$scheduler->addFiber($fiber1);

// 创建第二个 Fiber
$fiber2 = new Fiber(function (string $name, int $delay) {
echo "Fiber {$name} started.\n";
$result = asyncOperation($name, $delay);
echo "[Fiber {$name}] Received result: {$result}\n";
return "{$name} completed.";
});
$scheduler->addFiber($fiber2);

// 启动调度器
$scheduler->run();

?>

输出(大致顺序,实际可能因调度逻辑微调):

1
2
3
4
5
6
7
8
9
10
11
12
13
Fiber fiber1 started.
[Fiber fiber1] Starting operation, will take 3 seconds...
Fiber fiber2 started.
[Fiber fiber2] Starting operation, will take 2 seconds...
[Scheduler] Resuming Fiber 991807469 after simulated I/O delay... // Fiber 2 (2秒后)
[Fiber fiber2] Operation resumed after 2 seconds.
[Fiber fiber2] Received result: [Fiber fiber2] Result after 2 seconds.
Fiber finished with return: fiber2 completed.
[Scheduler] Resuming Fiber 991807469 after simulated I/O delay... // Fiber 1 (3秒后)
[Fiber fiber1] Operation resumed after 3 seconds.
[Fiber fiber1] Received result: [Fiber fiber1] Result after 3 seconds.
Fiber finished with return: fiber1 completed.
Scheduler finished.

注意:上面的调度器是一个非常简化的示例,旨在演示 Fibers 的暂停和恢复。一个真正的异步框架(如 Amphp, ReactPHP)会使用底层的 I/O 多路复用机制(epoll, kqueue 等)来高效地等待和调度就绪的 Fiber。

3.3 异常处理

Fibers 支持正常的异常处理机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php

$fiber = new Fiber(function (): void {
try {
echo "Fiber started.\n";
Fiber::suspend('paused');
echo "Fiber resumed.\n";
throw new Exception("Error inside Fiber!");
} catch (Exception $e) {
echo "Fiber caught exception: " . $e->getMessage() . "\n";
} finally {
echo "Fiber finally block.\n";
}
return 'Fiber finished successfully.';
});

$result = $fiber->start();
echo "Main received: " . $result . "\n";

// 通过 resume() 恢复 Fiber,它会继续执行到抛出异常的地方
// 异常在 Fiber 内部被捕获
$result2 = $fiber->resume();
echo "Main received after internal catch: " . $result2 . "\n";

$fiber2 = new Fiber(function (): void {
echo "Fiber2 started.\n";
Fiber::suspend();
echo "Fiber2 will never resume here.\n";
});

$fiber2->start();
try {
// 在 Fiber 外部向 Fiber 内部抛出异常
$fiber2->throw(new Exception("External error for Fiber2!"));
} catch (Exception $e) {
echo "Main caught exception when throwing to Fiber2: " . $e->getMessage() . "\n";
}

echo "Fiber2 status: " . ($fiber2->isTerminated() ? "Terminated" : "Running") . "\n";
?>

输出:

1
2
3
4
5
6
7
8
9
Fiber started.
Main received: paused
Fiber resumed.
Fiber caught exception: Error inside Fiber!
Fiber finally block.
Main received after internal catch: Fiber finished successfully.
Fiber2 started.
Main caught exception when throwing to Fiber2: External error for Fiber2!
Fiber2 status: Terminated

四、Fibers 与 Generators 的区别

特性 Generators (PHP 5.5+) Fibers (PHP 8.1+)
主要目的 迭代器,按需生成值,实现惰性求值。 轻量级并发原语,实现用户空间多任务,简化异步代码。
上下文切换 通过 yield 暂停,通过 send()next() 恢复。 通过 Fiber::suspend() 暂停,通过 Fiber::resume() 恢复。
控制流 控制权从生成器返回到调用者,由调用者推动生成器。 控制权在 Fiber 和其父 Fiber 之间切换,更加灵活和对称。
返回值 只能在迭代完成后通过 getReturn() 获取一个最终值。 start()resume() 直接返回 suspend() 的值,或者 Fiber 的最终返回值。
传递值 send() 传入值作为 yield 表达式结果。 suspend() 传出值,resume() 传入值作为 suspend() 结果。
语义 更偏向于数据流和迭代。 更偏向于控制流和任务调度。
复杂性 作为协程使用时,需要外部调度器管理其状态和切换逻辑。 更直接地提供协程机制,简化了异步库的底层实现。

五、Fibers 的适用场景

Fibers 本身只是一个低级原语。它们的真正价值在于作为构建高级异步框架(如 Amphp, ReactPHP)的基础。

  • 异步 I/O 操作
    • 网络请求(HTTP 客户端、数据库客户端)
    • 文件 I/O
    • 消息队列处理
  • 长运行任务
    • 在不阻塞主线程的情况下执行耗时计算(尽管 PHP 仍然是单线程的,但可以避免阻塞 I/O)。
  • 并发任务调度
    • 创建自定义的协程调度器,管理多个并发执行的任务。
  • Web 服务器
    • 在 PHP-FPM 模型下,每个请求依然是独立的。Fibers 主要用于优化单个请求内部的并发 I/O 效率。
    • 对于基于 Swoole 或 RoadRunner 的应用,Fibers 可以与这些服务器的事件循环更好地集成,实现真正的请求级并发。

六、使用 Fibers 的注意事项

  1. 不是并行编程:Fibers 实现的是协作式多任务,而不是并行(Parallelism)。在任何时刻,只有一个 Fiber 在 CPU 上运行。它们通过快速上下文切换来模拟并发,尤其适用于 I/O 密集型任务。
  2. 需要调度器:Fibers 本身并不会自动调度。开发者需要一个事件循环 (Event Loop) 或调度器来协调多个 Fibers 的执行,并在 I/O 就绪时恢复它们。例如,Amphp 和 ReactPHP 等异步框架已经集成了基于 Fibers 的调度器。
  3. 栈管理:每个 Fiber 都有自己的调用栈。虽然 Fibers 比线程轻量,但创建过多或深度嵌套的 Fibers 仍然会消耗内存。
  4. 透明性:Fibers 的设计目标之一是让异步代码看起来像同步代码。这意味着开发者在 Fiber 内部调用一个“阻塞”的 I/O 函数时,实际上它可能已经被底层框架包装成了 Fiber::suspend() + 事件循环等待 + Fiber::resume() 的模式,从而实现了非阻塞。
  5. 调试:由于执行流的非线性特性,调试 Fibers 可能会比调试传统同步代码更具挑战性。Xdebug 等调试工具可能需要更新以支持 Fibers 的上下文切换。

七、总结

PHP Fibers 是 PHP 语言发展史上的一个重要里程碑,它为 PHP 带来了原生的协程支持,极大地简化了异步非阻塞编程。通过 Fibers,开发者能够编写更清晰、更易维护的异步代码,特别是在处理 I/O 密集型应用时,能够显著提升性能和响应速度。虽然 Fibers 是低级原语,但它们是构建未来高性能 PHP 异步框架的基石,预示着 PHP 在并发和异步编程领域迈出了坚实的一步。