【一】.迭代器
迭代是指反复执行一个过程,每执行一次叫做一次迭代。比如下面的代码就叫做迭代:
1. <?php 2. $data = ['1', '2', '3']; 3. 4. foreach ($data as $value) 5. { 6. echo $value . PHP_EOL; 7. }
然后我们看看官方的迭代器接口:
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函数,代码如下:
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函数,代码如下:
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 多次,以便生成需要迭代的值。参考下面的代码:
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. }
其实你会发现生成器生成的东西和迭代器生成的一样,我们来看看这个生成器生成的对象到底是什么鬼,直接打印对象类型,判断是否是继承自迭代器,看代码:
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 后控制权回到外部。请看下面的代码:
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其实还支持同时发送数据和接收数据,代码如下:
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:
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:
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进行测试:
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发送会有问题,参考下面的代码:
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。