PHP 生成器、Fiber 与协程调度:从 yield 到协作式并发

PHP 生成器、Fiber 与协程调度实战:Iterator 接口与 yield 语义、惰性序列与内存优势、send/getReturn/throw 双向通信、大文件与数据库游标流式处理、Fiber 的挂起与恢复、状态机与事件循环协作、与 Swoole/ReactPHP 的对比、手写协作式任务调度器。

引言

很多人把生成器当成「写起来更爽的迭代器」,把 Fiber 当成「PHP 终于有协程了」——但两者背后是同一件事:让函数在中途「暂停」,把控制权交还给调用方。理解了这个机制,你才能写出内存恒定的大文件处理器、优雅的惰性管道,以及不依赖扩展的协作式调度器。本文从 yield 讲到 Fiber,再到事件循环协作与 Swoole/ReactPHP 的取舍。

前置:异步与常驻内存、性能调优、队列与调度。


目录


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() 查询。

维度GeneratorFiber
暂停点仅 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(用户态)ReactPHPSwoole
挂起粒度仅 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 ...; }
指定 keyyield $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 是原语,调度器才是大脑。


延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「php」更多文章

  1. PHP 多租户 SaaS 架构:隔离策略、数据作用域与按租户计费
  2. PHP 的 CQRS 与事件溯源:命令总线、事件存储与投影
  3. PHP 支付集成实战:Stripe、支付宝与微信支付的状态机与回调