【一】.迭代器
迭代是指反复执行一个过程,每执行一次叫做一次迭代。比如下面的代码就叫做迭代:
PHP
代码语言:javascript复制1. <?php
2. $data = ['1', '2', '3'];
3.
4. foreach ($data as $value)
5. {
6. echo $value . PHP_EOL;
7. }
然后我们看看官方的迭代器接口:
PHP
代码语言:javascript复制1. Iterator extends Traversable {
2. abstract public mixed current ( void )
3. abstract public scalar key ( void )
4. abstract public void next ( void )
5. abstract public void rewind ( void )
6. abstract public boolean valid ( void )
7. }
8.
9. /**
10. 方法说明
11. Iterator::current — 返回当前元素
12. Iterator::key — 返回当前元素的键
13. Iterator::next — 向前移动到下一个元素
14. Iterator::rewind — 返回到迭代器的第一个元素
15. Iterator::valid — 检查当前位置是否有效
16. */
先放下普通函数实现php自带的range函数,代码如下:
PHP
代码语言:javascript复制1. <?php
2. function newrange($low, $hign, $step = 1)
3. {
4. $ret = [];
5. for ($i = 0; $i < $hign; $i = $step)
6. {
7. $ret[] = $i;
8. }
9. return $ret;
10. }
11.
12. $result = newrange(0, 500000);
上面的代码没有用生成器,创建50w的数组占用内存14M
再放下使用生成器实现php自带的range函数,代码如下:
PHP
代码语言:javascript复制1. <?php
2.
3. class newrange implements Iterator
4. {
5. protected $low;
6. protected $high;
7. protected $step;
8. protected $current;
9.
10. public function __construct($low, $high, $step = 1)
11. {
12. $this->low = $low;
13. $this->high = $high;
14. $this->step = $step;
15. }
16.
17. //返回到迭代器的第一个元素
18. public function rewind()
19. {
20. $this->current = $this->low;
21. }
22.
23. //向前移动到下一个元素
24. public function next()
25. {
26. $this->current = $this->step;
27. }
28.
29. //返回当前元素
30. public function current()
31. {
32. return $this->current;
33. }
34.
35. //返回当前元素的键
36. public function key()
37. {
38. return $this->current 1;
39. }
40.
41. //检查当前位置是否有效
42. public function valid()
43. {
44. return $this->current <= $this->high;
45. }
46. }
47.
48. $result = new newrange(0, 500000, 1);
上面的代码使用了生成器实现,创建50w的数组占用内存0.09kb,性能差距多大。
由于普通函数是直接创建了50w的数组所以占用内存过大,而迭代器只是按照规则进行迭代,只有使用时才真正执行的时候才迭代值出来,所以省内存。
总结:迭代器提供的是一整套操作子数据的接口,foreach也就每次可以通过next移动指针来获取数据。我们迭代的过程是虽然是foreach语句中的代码块,假如把数组看做一个对象,foreach 实际上在每一次迭代过程都会调用该对象的一个方法,让数组在自己内部进行一次变动(迭代),随后通过另一个方法取出当前数组对象的键和值。你可以理解为$data对象实现了迭代器接口,已经存在上面的迭代器方法,而foreach是遵守迭代器规则的工具帮你自动迭代,不用自己调用next方法获取下一个元素
迭代器只提供了数据元素的迭代方式,当我们在处理超大数组的时候具有很大的性能优势,可以在网上搜索php迭代器,看看newrange函数的实现内存占用。
【二】.生成器
虽然迭代器只需要实现接口即可,但是我们还得实现接口所有的方法,十分繁琐。生成器提供了一种更容易的方法来实现简单的对象迭代。
PHP 官方文档:
生成器允许你在foreach代码块中写代码来迭代一组数据而不需要在内存中创建一个数组(因为那会使你的内存达到上限,或者会占据可观的处理时间)。相反,你可以写一个生成器函数,就像一个普通的自定义函数一样,。普通函数只返回一次值, 生成器函数可以根据需要yield 多次,以便生成需要迭代的值。参考下面的代码:
PHP
代码语言:javascript复制1. <?php
2.
3. function newrange($start, $limit, $step = 1)
4. {
5. for ($i = $start; $i <= $limit; $i = $step)
6. {
7. (yield $i 1 => $i);
8. }
9. }
10.
11. foreach (newrange(0, 500000, 1) as $key => $value)
12. {
13. echo 'key:' . $key . '=>' . 'value:' . $value . PHP_EOL;
14. }
其实你会发现生成器生成的东西和迭代器生成的一样,我们来看看这个生成器生成的对象到底是什么鬼,直接打印对象类型,判断是否是继承自迭代器,看代码:
PHP
代码语言:javascript复制1. <?php
2.
3. function newrange($start, $limit, $step = 1)
4. {
5. for ($i = $start; $i <= $limit; $i = $step)
6. {
7. (yield $i 1 => $i);
8. }
9. }
10.
11. $object = newrange(0, 500000);
12.
13. var_dump($object);//输出object(Generator)#1
14.
15. if($object instanceof Iterator)
16. {
17. var_dump('生成器生成的对象继承自迭代器'); //正常输出
18. }
结果证明了生成器生成的对象是继承自迭代器,这样就不难理解生成器的迭代了。
我们需要注意关键字yield,这是生成器的关键。foreach 每一次迭代过程都会从 yield 处取一个值,直到整个遍历过程不再存在 yield 为止的时候,遍历结束。
【三】.yield
重点内容:
yield 和 return 的区别,前者是暂停当前过程的执行并返回值,而后者是中断当前过程并返回值。暂停当前过程,意味着将处理权转交由上一级继续进行,直至上一级再次调用被暂停的过程,该过程则会从上一次暂停的位置继续执行。
这很像是一个操作系统的进程调度管理,多个进程在一个 CPU 核心上执行,在系统调度下每一个进程执行一段指令就被暂停,切换到下一个进程,这样看起来就像是同时在执行多个任务。
当然yield 更重要的特性是除了可以返回一个值以外,还能够接收一个值!
迭代器对象Generator 对象除了实现 Iterator 接口中的必要方法以外,还有一个 send 方法,这个方法就是向 yield 语句处传递一个值,同时从 yied 语句处继续执行,直至再次遇到 yield 后控制权回到外部。请看下面的代码:
PHP
代码语言:javascript复制1. <?php
2. function test()
3. {
4. while (true)
5. {
6. sleep(1);
7. echo(yield);
8. }
9. }
10.
11. $tester = test();
12. $tester->send('111');
13. $tester->send('222');
以上输出: 111 222
Yield其实还支持同时发送数据和接收数据,代码如下:
PHP
代码语言:javascript复制1. <?php
2. function test()
3. {
4. $i = 0;
5. while (true)
6. {
7. sleep(1);
8. echo (yield $i) . PHP_EOL;
9. }
10. }
11. $tester = test();
12.
13. //输出生成器(迭代器)当前的元素
14. $cur = $tester->current();
15. echo ($cur) . PHP_EOL;
16.
17. //向yield处发送数据
18. $tester->send('go');
19.
20. //输出生成器(迭代器)当前的元素
21. $cur = $tester->current();
22. echo ($cur) . PHP_EOL;
23.
24. //向yield处发送数据
25. $tester->send('end');
以上的结果会输出:
1
go
2
end
很多人会很疑惑这个执行过程我也是。
(1).$tester->current()执行后触发迭代器,在迭代器中执行.遇到yield触发返回值的代码(yield $i),此时相当于yield 1;把1的值直接返回出去了,并且执行权恢复到了外部,外部echo ($cur) . PHP_EOL输出了1
(2).外部继续执行到$tester->send('go'); 发送数据到yield处,由于是双向通信yield此时恢复到之前的yield位置接收到了数据并赋值给了$data,输出了go。输出go这步有人有疑问,不应该是赋值后直接把执行权给外部吗?记住这里接收数据会恢复到上次的yield没走完的部分会走完上次未完成的迭代再交给外部执行权。
(3).外部再次调用$tester->current()此时迭代器内部执行并且返回值再次给外部执行权
(4).外部再次发送$tester->send('end');数据给上次未走完的yield,yield收到值在内部打印输出end并走完迭代把执行权限给外部,外部无代码执行结束
【四】.基于yield实现协程任务调度
上面我们知道每个生成器函数都可以被暂停。那当我们创建多个生成器函数,然后把这些生成器函数全部放到一个队列里面,通过循环队列每次将每个生成器函数执行1次并暂停,然后判断是否执行完成,未执行完成重新放回队列,然后继续下一个任务,重复循环即可实现协程调度多个任务。
创建1个task.php:
PHP
代码语言:javascript复制1. <?php
2.
3. /**
4. * Task任务类
5. */
6. class Task
7. {
8. /**
9. * 任务是否执行过
10. */
11. protected $isRuned;
12.
13. /**
14. * 任务的生成器
15. * @var Generator
16. */
17. protected $coroutine;
18.
19. /**
20. * Task constructor.
21. * @param Generator $coroutine
22. */
23. public function __construct(Generator $coroutine)
24. {
25. $this->isRuned = false;
26. $this->coroutine = $coroutine;
27. }
28.
29. /**
30. * 判断是否执行完毕
31. */
32. public function valid()
33. {
34. return $this->coroutine->valid();
35. }
36.
37. /**
38. * 运行任务
39. */
40. public function run()
41. {
42. //未执行从头开始迭代
43. if (!$this->isRuned)
44. {
45. $this->isRuned = true;
46. $this->coroutine->current();
47. }
48. else
49. {
50. $this->coroutine->send(null);
51. }
52. }
53.
54. }
创建一个scheduler.php:
PHP
代码语言:javascript复制1. <?php
2.
3. class Scheduler
4. {
5. /**
6. * 保存任务的队列
7. * @var SplQueue
8. */
9. protected $taskQueue;
10.
11. /**
12. * Scheduler constructor.
13. */
14. public function __construct()
15. {
16. $this->taskQueue = new SplQueue();
17. }
18.
19. /**
20. * 增加一个任务到队列
21. * @param Generator $task
22. */
23. public function addTask(Generator $task)
24. {
25. $this->taskQueue->enqueue(new Task($task));
26. }
27.
28. /**
29. * 运行调度器
30. */
31. public function run()
32. {
33. while (!$this->taskQueue->isEmpty())
34. {
35. //从队列中取出任务
36. $task = $this->taskQueue->dequeue();
37. $task->run();
38.
39. //任务中的迭代未全部执行完成
40. if ($task->valid())
41. {
42. $this->taskQueue->enqueue($task);
43. }
44. }
45. }
46. }
创建一个test.php进行测试:
PHP
代码语言:javascript复制1. <?php
2. include 'scheduler.php';
3. include 'task.php';
4.
5. function task1()
6. {
7. for ($i = 1; $i <= 3; $i)
8. {
9. echo "This is task 1 $i" . PHP_EOL;
10. yield; //暂停执行
11. }
12. }
13.
14. function task2()
15. {
16. for ($i = 1; $i <= 3; $i)
17. {
18. echo "This is task 2 $i" . PHP_EOL;
19. yield; //暂停执行
20. }
21. }
22.
23.
24. //实例化调度器
25. $scheduler = new Scheduler();
26. $scheduler->addTask(task1());
27. $scheduler->addTask(task2());
28. $scheduler->run();
实际输出:
This is task 1 1
This is task 2 1
This is task 1 2
This is task 2 2
This is task 1 3
This is task 2 3
$this->isRuned是为了判断生成器函数是否执行(迭代)过,因为我们直接使用send发送会有问题,参考下面的代码:
PHP
代码语言:javascript复制1. <?php
2.
3. function gen()
4. {
5. yield 'a';
6. yield 'b';
7. }
8.
9. $gen = gen();
10. var_dump($gen->send(''));
以上输出:b ,为什么输出b呢?当我们直接使用send发送,实际上生成器隐式执行了renwind方法,并且忽略了返回值,因此使用isRuned来确保第一个yield被正确执行
实际上这样得协程当任务只实现了函数的暂停中断,但是当yield前是阻塞很久的代码,那这个协程意义就不大。同样推荐使用swoole。