实现了 Iterator 接口的对象可以使用 foreach
结构来访问,Iterator
接口所提供了 5 个方法:
1<?php
2Iterator extends Traversable {
3 /* Methods */
4 abstract public mixed current ( void ) //返回当前位置的元素
5 abstract public scalar key ( void ) //返回当前元素对应的key
6 abstract public void next ( void ) //移到指向下一个元素的位置
7 abstract public void rewind ( void ) //倒回到指向第一个元素的位置
8 abstract public boolean valid ( void ) //判断当前位置是否有效
9}
在使用 foreach
遍历实现这个接口的对象时,这里的某些方法会被隐式地调用。其中 next()
方法是控制元素移动的,current()
可以获取当前位置的元素。
在 php
中,yield
关键字只能在函数中使用,使用了 yield
关键字的函数都会返回一个 Generator
对象。我们可以直接使用 foreach
结构遍历这个 Generato
r 对象:
上面的代码输出:
11
22
33
Generator
对象的中可迭代的元素就是所有 yield
语句返回的值的集合,在这个示例中是 [1,2,3]
。
看起来跟数组很像,但它跟数组有本质的区别,遍历 Generator 对象的每次迭代都只会执行前一次 yield
语句之后的代码,
而且碰到 yield
语句就会返回一个值,相当于从 Generator
对象中返回,这有点像挂起一个进程(线程)的执行,然后又启动它继续执行,周而复始直到进程(线程)执行中止。
1<?php
2
3function foo()
4{
5 $end = 'end';
6 echo "break point 1\n";
7 yield 1;
8 echo "break point 2\n";
9 yield 2;
10 echo "break point 3\n";
11 yield 3;
12 echo $end;
13}
14while ($foo->valid()) {
15 echo $foo->current() . "\n";
16 $foo->next();
17}
输出:
1
2break point 1
31
4break point 2
52
6break point 3
73
8end
这里的 yield
相当于一个表达式,它需要跟 Generator
对象中的 send
方法配合使用。
send
方法接收一个参数,它会将这个参数的值传递给 Generator
对象并作为当前 yield
表达式的结果,同时还会恢复 Generator
对象的迭代:
1<?php
2function gen() {
3 $ret = yield 'yield1';
4 var_dump($ret);
5 $ret = yield 'yield2';
6 var_dump($ret);
7}
8$g = gen();
9var_dump($g->current());
10var_dump($g->send('ret1'));
11var_dump($g->send('ret2'));
输出如下:
1string(6) "yield1"
2string(4) "ret1"
3string(6) "yield2"
4string(4) "ret2"
5NULL
下面我们看一个通过 yield
输出 fibonacci
数列的示例:
1<?php
2function fibonacci()
3{
4 $a = 0;
5 $b = 1;
6 $count = 1;
7 while(true) {
8
9 $tmp = $a;
10 $a = $b;
11 $b = $tmp + $b;
12 $commend = yield [$count++, $b];
13
14 if('stop' === $commend) {
15 break;
16 }
17 }
18}
19
20$fibonacci = fibonacci();
21
22while($fibonacci->valid()) {
23
24 $current = $fibonacci->current();
25 $fibonacci->next();
26
27 echo $current[1]."\n";
28 // 输出第20个数时,发出命令停止输出
29 if(20 === $current[0]) {
30 $fibonacci->send('stop');
31 echo "end";
32 }
33}
该方法除了一次返回 fibonacci
数之外, 还有一个功能: 当接受到外部的 stop
信息后,会跳出循环,终止 yield
的输出:
11
22
33
45
58
613
721
834
955
1089
11144
12233
13377
14610
15987
161597
172584
184181
196765
2010946
21end
这与调用函数的体验是非常不同的,调用函数我们通常只能通过入参来影响函数的运行,而对于一个使用 yield
的生成的 Generator
对象,我们可以通过与之通信的方式随时干涉内部代码的运行!
使用更小的内存遍历集合
在我们编写某些脚本时,可能会需要一次从数据库取出大量的数据,通常我们会一次获取所有数据,然后再遍历处理。这种方式会把所有的数据全部加载到内存中,产生不必要的资源浪费;如果使用 yield
,遍历 Generator
对象,仅在需要的时候才获取数据,可以极大地减小内存开销。
我们通过一个示例来比较这两种方式地差异:这里我构造了一张有 50 万条记录的数据表,分别通过上面提到两种方式来遍历数据。
1<?php
2
3$pdo = new PDO($dsn, $username, $password);
4
5$t1 = microtime(true);
6
7$result = $pdo->query("select * from foo");
8
9$data = $result->fetchAll(); // 一次获取所有数据
10
11foreach ($data as $row) {
12
13 var_dump($row);
14}
15
16$t2 = microtime(true);
17
18echo '耗时' . round($t2 - $t1, 2) . '秒'."\n";
19echo '内存: ' . memory_get_usage() / 1024 . "K\n";
20
21
22$pdo = new PDO($dsn, $username, $password);
23
24$t1 = microtime(true);
25
26function getAll($pdo)
27{
28 $result = $pdo->query("select * from foo");
29
30 while ($row = $result->fetch()) {
31 yield $row;
32 }
33}
34
35// 遍历生成器逐条获取数据
36foreach (getAll($pdo) as $row) {
37
38 var_dump($row);
39}
40
41$t2 = microtime(true);
42
43echo '耗时' . round($t2 - $t1, 2) . '秒'."\n";
44echo '内存: ' . memory_get_usage() / 1024 . "K\n";
使用一般方式遍历:
耗时 46.15 秒
内存: 313652.9765625K
使用 yield 的 Generator 对象遍历:
耗时 48.81 秒
内存: 392.234375K
可以看出,使用 Generator
对象遍历占用的内存要小得多。
通过 Generator
的 send()
方法可以外部过程实现与任务内部的通信,这为协程开发提供了可能。
下面我们看一个简单的协程的实现:
首先需要对 Generato
生成器做一个封装:
1<?php
2class Task {
3 protected $taskId; // 任务Id
4 protected $coroutine; // 任务迭代器
5 protected $sendValue = null; // 下一次向任务迭代器发送的消息
6 protected $beforeFirstYield = true; //首次迭代标识
7
8 public function __construct($taskId, Generator $coroutine) {
9 $this->taskId = $taskId;
10 $this->coroutine = $coroutine;
11 }
12
13 public function getTaskId() {
14 return $this->taskId;
15 }
16
17 public function setSendValue($sendValue) {
18 $this->sendValue = $sendValue;
19 }
20
21 public function run() {
22 if ($this->beforeFirstYield) {
23 $this->beforeFirstYield = false;
24 return $this->coroutine->current();
25 } else {
26 $retval = $this->coroutine->send($this->sendValue);
27 $this->sendValue = null;
28 return $retval;
29 }
30 }
31
32 public function isFinished() {
33 return !$this->coroutine->valid();
34 }
35}
这里我们简单说一下 run()
方法:它的主要功能是调用 Generator
里的 send()
方法推送信息给生成器,接收并返回生成器的返回值;
使用 $beforeFirstYield
属性是为了避免直接调用生成器 send()
方法会丢失首个 yield
返回值的问题;
接着我们要实现一个任务调度器来规划任务:
1<?php
2/**
3 * 调度器
4 * Class Scheduler
5 */
6class Scheduler {
7 protected $maxTaskId = 0; // 任务自增Id
8 protected $taskMap = []; // taskId => task
9 protected $taskQueue; // 任务队列
10
11 public function __construct() {
12 $this->taskQueue = new SplQueue();
13 }
14
15 public function newTask(Generator $coroutine) {
16 $tid = ++$this->maxTaskId;
17 $task = new Task($tid, $coroutine);
18 $this->taskMap[$tid] = $task;
19 $this->schedule($task);
20 return $tid;
21 }
22
23 public function schedule(Task $task) {
24 $this->taskQueue->enqueue($task);
25 }
26
27 public function run() {
28 while (!$this->taskQueue->isEmpty()) {
29 $task = $this->taskQueue->dequeue(); // 任务出队
30 $task->run(); // 调用run()方法执行任务片段
31
32 if ($task->isFinished()) { // 任务执行完毕,从map中删除
33 unset($this->taskMap[$task->getTaskId()]);
34 } else {
35 $this->schedule($task); // 任务未执行完,继续入队
36 }
37 }
38 }
39}
我们这里任务调度的逻辑很简单,从任务调度队列中顺序取出任务并执行任务,如果任务执行完毕,就从 map
中移除任务,否则继续将任务入队。
接下来注册具体的任务逻辑:
1<?php
2function task1() {
3 for ($i = 1; $i <= 10; ++$i) {
4 echo "This is task 1 iteration $i.\n";
5 yield;
6 }
7}
8
9function task2() {
10 for ($i = 1; $i <= 5; ++$i) {
11 echo "This is task 2 iteration $i.\n";
12 yield;
13 }
14}
15
16$scheduler = new Scheduler;
17
18$scheduler->newTask(task1());
19$scheduler->newTask(task2());
20
21$scheduler->run();
执行结果如下:
1This is task 1 iteration 1.
2This is task 2 iteration 1.
3This is task 1 iteration 2.
4This is task 2 iteration 2.
5This is task 1 iteration 3.
6This is task 2 iteration 3.
7This is task 1 iteration 4.
8This is task 2 iteration 4.
9This is task 1 iteration 5.
10This is task 2 iteration 5.
11This is task 1 iteration 6.
12This is task 1 iteration 7.
13This is task 1 iteration 8.
14This is task 1 iteration 9.
15This is task 1 iteration 10.
总结
如果在函数中使用 yield 关键字,那么这个函数的返回值会变成一个 Generator
对象。
通过 current()
或 send()
方法可以运行函数定义的代码片段,代码片段会在遇到 yield
关键字时中断代码运行,保留上下文,并返回 yield
后声明的返回值。
使用 send()
方法可以向 Generator 对象传递信息。
对于某些顺序遍历集合的场景,使用 yield 定义的 Generator
生成器能节约内存开销。
通过 Generator
生成器可以实现 php
协程,不过具体的调度逻辑与通信逻辑需要用户自行实现。