引言
很多人把生成器当成「写起来更爽的迭代器」,把 Fiber 当成「PHP 终于有协程了」——但两者背后是同一件事:让函数在中途「暂停」,把控制权交还给调用方。理解了这个机制,你才能写出内存恒定的大文件处理器、优雅的惰性管道,以及不依赖扩展的协作式调度器。本文从 yield 讲到 Fiber,再到事件循环协作与 Swoole/ReactPHP 的取舍。
目录
- 1. 迭代器与生成器的本质
- 2. yield 的关键字语义
- 3. 惰性序列与内存优势
- 4. send、getReturn 与 throw
- 5. 生成器实战:大文件与 CSV 流式
- 6. Fiber:真正的挂起与恢复
- 7. Fiber 调度器与事件循环协作
- 8. 与 Swoole 和 ReactPHP 的对比
- 9. 实战:协作式任务调度器
- 10. 速查表与一句话记忆
- 延伸阅读
1. 迭代器与生成器的本质
1.1 手写 Iterator vs 生成器
要遍历自定义结构,PHP 要求实现 Iterator 的五个方法;而同一个倒计时用生成器只要三行:
final class Countdown implements Iterator
{
private int $current;
public function __construct(private int $start) { $this->current = $start; }
public function current(): mixed { return $this->current; }
public function key(): mixed { return $this->start - $this->current; }
public function next(): void { $this->current--; }
public function rewind(): void { $this->current = $this->start; }
public function valid(): bool { return $this->current > 0; }
}
function countdown(int $start): Generator
{
for ($i = $start; $i > 0; $i--) { yield $i; }
}
PHP 在编译期把含 yield 的函数改造成状态机:局部变量、程序计数器、调用栈都存进 Generator 对象。函数不再一次跑完,而是「跑到 yield 就停」。
| 维度 | 手写 Iterator | 生成器 |
|---|---|---|
| 代码量 | 5 个方法 | 1 个 yield |
| 状态保存 | 手动 | 自动(栈 + 局部变量) |
| 可读性 | 差(状态打散) | 好(顺序即逻辑) |
| 双向通信 | 无 | 有(send) |
记忆:生成器是「自动实现 Iterator 的函数」——
yield处暂停,PHP 把局部变量与程序计数器存进 Generator 对象,下次从暂停点继续。
2. yield 的关键字语义
2.1 四种写法
function demo(): Generator
{
yield 'a'; // key 自动从 0 递增
yield 'x' => 'b'; // 指定 key
yield; // 产出 null
yield from [1, 2, 3]; // 委托给另一个可迭代对象
}
yield from 会把子生成器的 key/value 直接透传,比循环 foreach ... yield 快,也支持嵌套委托。
2.2 yield 是表达式而非语句
yield $x 的返回值是「调用方 send 进来的值」,这正是它能做协程的根源:
$gen = (function (): Generator {
while (true) {
$line = yield; // 挂起,等待外部 send
echo "收到: {$line}\n";
}
})();
$gen->current(); // 启动到第一个 yield
$gen->send('hello'); // 恢复,$line = 'hello'
生成器还可以 return:遍历结束后用 $gen->getReturn() 取到该值(return $total;)。
记忆:
yield是表达式——yield $k => $v产出值、yield无参产出 null、yield from委托子生成器、send($v)成为它的返回值、return的值用getReturn()取。
3. 惰性序列与内存优势
3.1 内存对比
$nums = range(1, 10_000_000); // 数组:约 300+ MB 一次性进内存
function nums(int $max): Generator
{
for ($i = 1; $i <= $max; $i++) { yield $i; } // 生成器:一次只产一个
}
| 方式 | 内存占用 | 首字节时间 | 适合 |
|---|---|---|---|
range() 数组 | O(n) | 慢(先全建) | 小数据 |
| 生成器 | O(1) | 快(惰性) | 大流/无限流 |
3.2 无限序列与惰性管道
生成器可以表示「永不结束」的序列——数组做不到:
function fibonacci(): Generator
{
[$a, $b] = [0, 1];
while (true) { yield $a; [$a, $b] = [$b, $a + $b]; }
}
map/filter 也可以用生成器串成管道:链式组合时每个元素走完全管道才产出下一个,零中间数组。
记忆:生成器是 O(1) 内存的惰性序列——
range()先建全量、生成器边用边产;无限序列只能靠生成器表达;map/filter串管道可避免中间数组。
4. send、getReturn 与 throw
4.1 双向通道
生成器不只是「产出」,还是「接收」——send() 的值成为暂停处 yield 表达式的返回值:
function accumulator(): Generator
{
$total = 0;
while (true) {
$x = yield $total; // 产出 total,同时接收下一个 x
if ($x === null) break;
$total += $x;
}
return $total;
}
$acc = accumulator();
$acc->current(); // 启动 → 产出 0
$acc->send(10); // 产出 10
$acc->send(5); // 产出 15
4.2 throw 与协程雏形
$gen->throw($e) 会把异常注入到被暂停的 yield 处抛出,让生成器像协程一样「接收外部事件」:
$g = (function (): Generator {
try { yield 1; } catch (RuntimeException $e) { echo "被注入: {$e->getMessage()}"; }
})();
$g->current();
$g->throw(new RuntimeException('boom')); // 在暂停点抛出
调度器只要循环 send() 结果,就得到了一套「无栈协程」——这是 ReactPHP/Swoole 早期协程实现的基石。
记忆:生成器是双向通道——
send($v)成为yield表达式的值、throw()在暂停点注入异常、getReturn()取最终返回;这就是最朴素的协程。
5. 生成器实战:大文件与 CSV 流式
5.1 逐行读超大文件
function readLines(string $path): Generator
{
$fh = fopen($path, 'rb');
try {
while (($line = fgets($fh)) !== false) {
yield rtrim($line, "\r\n");
}
} finally {
fclose($fh); // 生成器被提前 break 时也会执行
}
}
foreach (readLines('/var/log/huge.log') as $line) {
if (str_contains($line, 'ERROR')) { echo $line, PHP_EOL; }
}
finally 保证即使循环 break 提前退出,文件句柄也会关闭——这是生成器相对「回调式流」的巨大优势。
5.2 CSV 流式与数据库游标
CSV 导入同理:fgetcsv 边读边 yield array_combine($header, $row),一次只驻留一行。数据库侧,Laravel 的 Model::cursor() 就是生成器——它用 PDO 的 unbuffered 查询一次取一行,避免 get() 把整表读进内存:
foreach (User::where('active', 1)->cursor() as $user) {
$user->syncToCrm(); // 百万行也不爆内存
}
记忆:大文件用
fgets+ 生成器逐行产、CSV 用fgetcsv边读边 yield、数据库用cursor()流式;生成器的finally保证提前退出时释放资源。
6. Fiber:真正的挂起与恢复
6.1 生成器的局限与 Fiber 的补位
生成器只能在 yield 处暂停,且只能从「最外层」恢复——嵌套函数里无法独立暂停。Fiber 补上了这块:它能保存整条调用栈。
$fiber = new Fiber(function (string $name): string {
echo "开始 {$name}\n";
$paused = Fiber::suspend('暂停中'); // 挂起,把值交给 resume 方
echo "恢复,收到 {$paused}\n";
return '完成';
});
$ret = $fiber->start('A'); // 打印「开始 A」,返回 '暂停中'
$fiber->resume('go'); // 打印「恢复,收到 go」
echo $fiber->getReturn(); // 完成
6.2 生命周期与差异
状态机为 created → running → suspended → terminated,可用 isStarted() / isSuspended() / isTerminated() / isRunning() 查询。
| 维度 | Generator | Fiber |
|---|---|---|
| 暂停点 | 仅 yield | 任意位置 Fiber::suspend() |
| 嵌套调用 | 不能跨函数挂起 | 可以(整条调用栈被保存) |
| 双向传值 | send() | resume($v) |
| 异常注入 | throw() | Fiber::throw() |
| 用途 | 迭代/流式 | 协程/异步 |
记忆:Fiber 能保存「整条调用栈」——
Fiber::suspend()可在任意深度挂起、resume()从原处恢复;生成器只能在外层yield暂停,这是二者的本质分野。
7. Fiber 调度器与事件循环协作
7.1 为什么需要调度器
Fiber 只提供「挂起/恢复」原语,谁来决定何时恢复是调度器的事。典型模式:Fiber 遇到 I/O 时 suspend,把「I/O 完成后再 resume」注册进事件循环。
final class Scheduler
{
private SplQueue $tasks;
public function __construct() { $this->tasks = new SplQueue(); }
public function run(): void
{
while (!$this->tasks->isEmpty()) {
[$fiber, $value] = $this->tasks->dequeue();
$ret = $fiber->isStarted() ? $fiber->resume($value) : $fiber->start($value);
if ($ret !== null) { $this->tasks->enqueue([$fiber, null]); }
}
}
}
7.2 与事件循环结合
$fiber = new Fiber(function () {
$body = Fiber::suspend(); // 等 HTTP 响应
echo strlen($body);
});
$http->get('https://example.com', fn ($body) => $fiber->resume($body));
$fiber->start();
Amphp v3、ReactPHP 的 Fiber 适配层都是这个套路:Fiber 负责「暂停业务代码」,事件循环负责「决定何时继续」。
记忆:Fiber 只是原语,调度器才是大脑——业务在 I/O 处
suspend,事件循环在回调里resume;Amphp/ReactPHP 的 Fiber 集成就是这个模式。
8. 与 Swoole 和 ReactPHP 的对比
| 维度 | 纯生成器 | Fiber(用户态) | ReactPHP | Swoole |
|---|---|---|---|---|
| 挂起粒度 | 仅 yield | 任意调用栈 | 回调/协程 | 协程(C 扩展) |
| 依赖 | 无 | 无(PHP 8.1+) | Composer 包 | PHP 扩展 |
| 性能 | 低 | 中 | 中 | 高 |
| 生态 | 迭代/流式 | 通用协程 | 事件驱动 I/O | 全栈常驻 |
| 调试难度 | 低 | 中 | 中高 | 高 |
选型:只处理流式数据 → 生成器足矣;想自己控制调度、不装扩展 → Fiber + 自研调度器;要成熟的事件驱动 I/O → ReactPHP / Amp;要极致性能与常驻内存 → Swoole / FrankenPHP。
一个常见误解:Swoole 协程不是 Fiber——它由 C 扩展在 Runtime::enableCoroutine() 后自动把阻塞 I/O 改写成协程调度,业务代码几乎无感;而 Fiber 需要你显式 suspend/resume,或依赖上层框架封装。
记忆:生成器管流式、Fiber 管通用协程、ReactPHP 管事件驱动 I/O、Swoole 管极致性能——Swoole 协程由扩展自动改写 I/O,Fiber 需要显式挂起。
9. 实战:协作式任务调度器
9.1 轮转调度实现
final class RoundRobin
{
/** @var Fiber[] */
private array $fibers = [];
public function spawn(callable $task): void { $this->fibers[] = new Fiber($task); }
public function run(): void
{
while ($this->fibers) {
foreach ($this->fibers as $i => $fiber) {
if (!$fiber->isStarted()) { $fiber->start(); }
elseif (!$fiber->isTerminated()) { $fiber->resume(); }
if ($fiber->isTerminated()) { unset($this->fibers[$i]); }
}
$this->fibers = array_values($this->fibers);
}
}
}
$s = new RoundRobin();
$s->spawn(function () { for ($i = 0; $i < 3; $i++) { echo "A{$i} "; Fiber::suspend(); } });
$s->spawn(function () { for ($i = 0; $i < 3; $i++) { echo "B{$i} "; Fiber::suspend(); } });
$s->run(); // A0 B0 A1 B1 A2 B2
9.2 关键观察与取舍
输出交错为 A0 B0 A1 B1 ...——两个「函数」在单线程里交替执行,没有线程、没有锁,却实现了逻辑上的并发。这就是协作式并发的全部魔法:谁主动 suspend,谁才让出 CPU。
| 维度 | 协作式(Fiber) | 抢占式(多线程/多进程) |
|---|---|---|
| 让出时机 | 代码显式 suspend | 由调度器强制 |
| 数据竞争 | 天然无(单线程) | 需锁保护 |
| 阻塞风险 | 一处阻塞拖垮全部 | 互不影响 |
| 适用 | I/O 密集 | CPU 密集 |
记忆:Fiber 调度的本质是「轮转 + 主动让出」——
suspend让出、resume继续,单线程内实现交错执行;代价是任何一处阻塞都会拖垮整个调度器。
10. 速查表与一句话记忆
| 需求 | 做法 |
|---|---|
| 惰性序列 | function f(): Generator { yield ...; } |
| 指定 key | yield $k => $v |
| 委托子生成器 | yield from $inner |
| 双向传值 | $gen->send($v) / yield 表达式 |
| 注入异常 | $gen->throw($e) |
| 取返回值 | $gen->getReturn() |
| 大文件流式 | fgets + yield + finally |
| 数据库流式 | Model::cursor() |
| 任意位置挂起 | Fiber::suspend() / $fiber->resume() |
| 状态查询 | isStarted/isSuspended/isTerminated |
一句话记忆:生成器是「自动实现 Iterator 的函数」,yield 处保存局部变量与程序计数器,O(1) 内存做惰性序列与流式(fgets/fgetcsv/cursor()),send/getReturn/throw 让它成为最朴素的协程;Fiber 则能保存整条调用栈、在任意深度 suspend/resume,配上事件循环就是协作式调度器——Fiber 是原语,调度器才是大脑。
延伸阅读
- 异步编程与常驻内存:Swoole 与 ReactPHP
- 性能调优:OPcache、JIT 与内存
- 队列与任务调度
- 消息队列与事件驱动
- PHP 官方生成器文档
- PHP Fiber RFC
- Node.js 专题 — 事件循环与异步模型对照
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。