本文目录导读:

我们来详细探讨一下 PHP项目生成器(Generator) 和 协程(Coroutine) 这两个概念,它们之间有着非常紧密的联系。
生成器是PHP实现协程的基础设施,而协程是生成器的一种高级应用模式。
生成器 (Generator)
生成器提供了一种更简单、更节省内存的方式来处理迭代数据流,而不需要预先构建一个完整的数组。
核心概念:
yield关键字:这是生成器的灵魂。yield类似于return,但不同之处在于,yield会暂停函数的执行并返回一个值给调用者,同时保存函数的内部状态,当再次请求下一个值时,函数会从暂停的地方继续执行。Generator对象:任何包含yield关键字的函数在被调用时,都会返回一个Generator对象,这个对象实现了Iterator接口,可以用foreach循环来遍历。
主要优点:
- 内存效率:处理大数据集或文件时,不需要将所有数据加载到内存中,一次只生成一个数据项。
- 延迟计算:数据是按需生成的,直到真正需要时才进行计算。
基本示例:
<?php
// 一个生成器函数
function generateNumbers($start, $end) {
for ($i = $start; $i <= $end; $i++) {
// 注意这里的 yield,它暂停了函数
yield $i;
}
}
// 调用生成器函数,得到一个 Generator 对象
$numbers = generateNumbers(1, 1000000);
// 现在你可以像遍历数组一样遍历它,但内存占用极低
// 因为它一次只生成一个数字
foreach ($numbers as $number) {
echo $number . "\n";
if ($number >= 5) break; // 可以随时停止,无需生成后续数据
}
// 内存对比:如果用一个数组 range(1, 1000000),会占用约 100MB 内存
// 而生成器版本只占用很少的内存(约几百字节)
协程 (Coroutine)
协程是一种更通用的控制流机制,它允许一个计算过程(函数或闭包)在执行过程中被暂停,然后让出控制权给另一个计算过程,并在稍后恢复执行,协程的暂停和恢复点可以携带数据。
生成器是如何成为协程基础的?
PHP 的生成器在 yield 时暂停了函数的执行,这一点与协程的核心行为(挂起/恢复)完美契合,基本的生成器只能单向传递数据(从生成器到调用者)。真正的协程需要双向通信。
发送数据给生成器:$generator->send()
这是从生成器到协程的关键一步。send() 方法允许调用者向生成器函数内部传递一个值,这个值会成为 yield 表达式的返回值。
<?php
function logger() {
while (true) {
// 暂停,等待外部通过 send() 传入数据
$message = yield;
echo "Logging: " . $message . "\n";
}
}
// 创建生成器
$logger = logger();
// 启动生成器,执行到第一个 yield
$logger->current(); // 或者 $logger->rewind();
// 通过 send() 向生成器发送数据
$logger->send("User logged in"); // 输出: Logging: User logged in
$logger->send("Data saved"); // 输出: Logging: Data saved
这个 send() 方法让生成器从“数据生产者”变成了“可暂停的代码块”,这就是协程的本质。
生成器与协程的关系
| 特性 | 生成器 (Generator) | 协程 (Coroutine) |
|---|---|---|
| 本质 | 一种语法结构/迭代器 | 一种更通用的控制流模式 |
| 核心机制 | 暂停和恢复 (yield) |
暂停和恢复 (yield) |
| 数据流 | 单向(从生成器到调用者) | 双向(调用者和协程可以互相发送/接收数据) |
| 关系 | 是 PHP 实现协程的基础设施 | 是生成器的一种高级应用 |
send() 方法 |
不常用(通常只用 foreach) |
核心(实现双向通信的关键) |
一句话总结:
PHP 没有独立的“协程”关键字。当一个生成器通过 send() 方法和调用者进行双向数据通信时,它就被当作一个协程来使用了。
实际应用场景
-
生成器的典型应用:
- 处理大文件:逐行读取一个巨大的日志文件或CSV文件,而不一次性加载到内存。
- 数据管道:创建一系列生成器,每个生成器对数据流进行一级处理(过滤、转换、聚合)。
- 无限序列:生成斐波那契数列、素数等理论上无限的序列。
-
协程的典型应用:
- 协程调度器 (Scheduler):这是最强大的应用,你可以创建一个任务调度器,让多个任务(每个任务都是一个生成器/协程)协作执行,当一个任务遇到 I/O 操作(如读取文件、查询数据库、发送HTTP请求)时,它可以
yield给调度器,让调度器去执行其他就绪的任务,从而实现异步非阻塞的效果(虽然在PHP中默认是同步阻塞的,但协程调度器可以管理这种“伪并发”)。 - 状态机:可以用协程来实现一个清晰的状态机,每个
yield代表一个状态,send()传递触发状态转换的事件。 - 受保护的资源访问:实现一个简单的协程同步原语(如信号量)。
- 协程调度器 (Scheduler):这是最强大的应用,你可以创建一个任务调度器,让多个任务(每个任务都是一个生成器/协程)协作执行,当一个任务遇到 I/O 操作(如读取文件、查询数据库、发送HTTP请求)时,它可以
一个协程调度器的简化示例
为了让你感受协程的真正威力,我们实现一个最简化的协程调度器:
<?php
function task1() {
for ($i = 1; $i <= 3; $i++) {
echo "Task 1 - step $i\n";
// 主动让出执行权
yield;
}
}
function task2() {
for ($i = 1; $i <= 3; $i++) {
echo "Task 2 - step $i\n";
yield;
}
}
// 简易调度器
$scheduler = new SplQueue(); // 使用队列来管理任务
$scheduler->enqueue(task1());
$scheduler->enqueue(task2());
while (!$scheduler->isEmpty()) {
$task = $scheduler->dequeue(); // 取出一个任务
$task->send(''); // 恢复任务执行,直到遇到下一个 yield
if ($task->valid()) { // 检查任务是否完成 (还有 yield 吗?)
$scheduler->enqueue($task); // 如果没完成,重新加入队列
}
}
// 输出:
// Task 1 - step 1
// Task 2 - step 1
// Task 1 - step 2
// Task 2 - step 2
// Task 1 - step 3
// Task 2 - step 3
在这个例子里,task1 和 task2 交替执行,看起来像是“并发”的,这就是协程调度的核心思想。
- 生成器是 PHP 自带的一种语法糖,用于高效迭代,它提供了暂停的能力。
- 协程是建立在生成器(特别是
send()方法)之上的编程模式,它实现了暂停、恢复和双向通信。 - PHP 没有内置的
async/await或协程关键字,它的协程能力完全是通过 生成器 (yield) 和Generator::send()方法模拟和实现的。 - 重要的限制:PHP 默认的生成器/协程是协作式的,它们必须显式地
yield才能让出CPU,它们没有真正的“异步 I/O 驱动”能力,如果你在一个协程里执行file_get_contents(),整个PHP进程会阻塞住,直到文件读取完毕,要实现真正的异步,你需要结合像 ReactPHP, Swoole, Amp 这样的扩展或库,它们底层使用了事件循环和更底层的 I/O 多路复用。