前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >PHP yield PHP协程,PHP协程用法学习

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

原创
作者头像
高久峰
发布2024-04-20 12:53:15
860
发布2024-04-20 12:53:15
举报

【一】.迭代器

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

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。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档