PHP yield PHP协程,PHP协程用法学习

2024-04-20 12:53:15 浏览数 (1)

【一】.迭代器

迭代是指反复执行一个过程,每执行一次叫做一次迭代。比如下面的代码就叫做迭代:

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。

0 人点赞