填坑之PHP的yield和协程在一起的日子里

首先是,这是我第一次把公众号文章复制粘贴到github来。

其次是,很久很久之前,我挖了一个yield的一个坑,自己挖的坑自己填,不然迟早会把自己埋掉。

最后是,如果想看之前那个坑,请发送“yield”给README中的公众号,我开通了高大上的自动回复功能,稀罕地不得了!

PS:那篇文章中在最后我犯了一个错误,误下了一个结论:foreach中不能使用send并猜测这是PHP的bug,实际上并不是,真实的原因粗暴简单的理解就是send会让生成器继续执行一次导致。这件事情告诉我们:

除了装逼之外,甩锅也是有打脸风险的

那篇坑里,内容和你能在百毒上搜索到的大多数文章都是差不多的,不过我那篇坑标题起得好:《yield是个什么玩意(上)》,也就是暗示大家还有下篇,所以起标题也是需要一定技术含量的。

我坚信,在座的各位辣鸡在看完上篇坑文后最想说的注定是泰迪熊这句话(这是文化属性,不以各位的意志而转移):

填坑之PHP的yield和协程在一起的日子里_第1张图片

回到今天主旨上来,强调几点:

  • 虽然文章标题中有“yield和协程”这样的关键字,但实际上yield并不是协程,看起来有不少人直接将yield和协程划了等号。yield的本质是生成器,英文名字叫做Generator。

  • yield只能用在function中,但用了yield就已经不是传统意义上的function了,同时如果你企图在function之外的其他地方用yield,你会被打脸。

  • yield的最重要作用就是:自己中断一坨代码的执行,然后主动让出CPU控制权给路人甲;然后又能通过一些方式从刚才中断的地方恢复运行。这个就比较屌了,假如你请求了一个费时10s的服务器API,此时是可以让出CPU给路人甲。粗暴地说上面的过程就算是协程的基本概念。

多线程和多进程都是操作系统参与的调度,而协程是用户自主实现的调度,协程的关键点实际上是“用户层实现自主调度”,大概有“翻身农奴把歌唱”的意思。

下面我通过一坨代码来体会一把“翻身农奴”,你们感受一下:

 
  1. function gen1() {
  2. for( $i = 1; $i <= 10; $i++ ) {
  3. echo "GEN1 : {$i}".PHP_EOL;
  4. // sleep没啥意思,主要就是运行时候给你一种切实的调度感,你懂么
  5. // 就是那种“你看!你看!尼玛,我调度了!卧槽”
  6. sleep( 1 );
  7. // 这句很关键,表示自己主动让出CPU,我不下地狱谁下地狱
  8. yield;
  9. }
  10. }
  11. function gen2() {
  12. for( $i = 1; $i <= 10; $i++ ) {
  13. echo "GEN2 : {$i}".PHP_EOL;
  14. // sleep没啥意思,主要就是运行时候给你一种切实的调度感,你懂么
  15. // 就是那种“你看!你看!尼玛,我调度了!卧槽”
  16. sleep( 1 );
  17. // 这句很关键,表示自己主动让出CPU,我不下地狱谁下地狱
  18. yield;
  19. }
  20. }
  21. $task1 = gen1();
  22. $task2 = gen2();
  23. while( true ) {
  24. // 首先我运行task1,然后task1主动下了地狱
  25. echo $task1->current();
  26. // 这会儿我可以让task2介入进来了
  27. echo $task2->current();
  28. // task1恢复中断
  29. $task1->next();
  30. // task2恢复中断
  31. $task2->next();
  32. }

上面代码执行结果如下图:

填坑之PHP的yield和协程在一起的日子里_第2张图片

虽然我话都说到这里了,但是肯定还是有人get不到“所以,到底发生了什么?”。你要知道,如果function gen1和function gen2中没有yield,而是普通函数,你是无法中断其中的for循环的,诸如下面这样的代码:

 
  1. function gen1() {
  2. for( $i = 1; $i <= 10; $i++ ) {
  3. echo "GEN1 : {$i}".PHP_EOL;
  4. sleep( 1 );
  5. }
  6. }
  7. function gen2() {
  8. for( $i = 1; $i <= 10; $i++ ) {
  9. echo "GEN2 : {$i}".PHP_EOL;
  10. }
  11. }
  12. gen1();
  13. gen2();
  14. // 看这里,看这里,看这里!
  15. // 上面的代码一旦运行,一定是先运行完gen1函数中的for循环
  16. // 其次才能运行完gen2函数中的for循环,绝对不会出现
  17. // gen1和gen2交叉运行这种情况

填坑之PHP的yield和协程在一起的日子里_第3张图片

我似乎已然精通了yield

写到这里后我也开始蹩了,和以往的憋了三天蹦不出来个屁有所不同,我这次蹩出了一个比较典型的应用场景:curl。下面我们基于上面那坨辣鸡代码将gen1修改为一个耗时curl网络请求,gen2将向一个文本文件中写内容,我们的目的就是在耗时的curl开始后主动让出CPU,让gen2去写文件,以实现CPU的最大化利用。

 
  1. $ch1 = curl_init();
  2. // 这个地址中的php,我故意sleep了5秒钟,然后输出一坨json
  3. curl_setopt( $ch1, CURLOPT_URL, "http://www.selfctrler.com/index.php/test/test1" );
  4. curl_setopt( $ch1, CURLOPT_HEADER, 0 );
  5. $mh = curl_multi_init();
  6. curl_multi_add_handle( $mh, $ch1 );
  7. function gen1( $mh, $ch1 ) {
  8. do {
  9. $mrc = curl_multi_exec( $mh, $running );
  10. // 请求发出后,让出cpu
  11. yield;
  12. } while( $running > 0 );
  13. $ret = curl_multi_getcontent( $ch1 );
  14. echo $ret.PHP_EOL;
  15. return false;
  16. }
  17. function gen2() {
  18. for ( $i = 1; $i <= 10; $i++ ) {
  19. echo "gen2 : {$i}".PHP_EOL;
  20. file_put_contents( "./yield.log", "gen2".$i, FILE_APPEND );
  21. yield;
  22. }
  23. }
  24. $gen1 = gen1( $mh, $ch1 );
  25. $gen2 = gen2();
  26. while( true ) {
  27. echo $gen1->current();
  28. echo $gen2->current();
  29. $gen1->next();
  30. $gen2->next();
  31. }

上面的代码,运行以后,我们再等待curl发起请求的5秒钟内,同时可以完成文件写入功能,如果换做平时的PHP程序,就只能是先阻塞等待curl拿到结果后才能完成文件写入。

文章太长,就像“老太太的裹脚布一样,又臭又长”,所以,最后再对代码做个极小幅度的改动就收尾不写了!

 
  1. $ch1 = curl_init();
  2. // 这个地址中的php,我故意sleep了5秒钟,然后输出一坨json
  3. curl_setopt( $ch1, CURLOPT_URL, "http://www.selfctrler.com/index.php/test/test1" );
  4. curl_setopt( $ch1, CURLOPT_HEADER, 0 );
  5. $mh = curl_multi_init();
  6. curl_multi_add_handle( $mh, $ch1 );
  7. function gen1( $mh, $ch1 ) {
  8. do {
  9. $mrc = curl_multi_exec( $mh, $running );
  10. // 请求发出后,让出cpu
  11. $rs = yield;
  12. echo "外部发送数据{$rs}".PHP_EOL;
  13. } while( $running > 0 );
  14. $ret = curl_multi_getcontent( $ch1 );
  15. echo $ret.PHP_EOL;
  16. return false;
  17. }
  18. function gen2() {
  19. for ( $i = 1; $i <= 10; $i++ ) {
  20. echo "gen2 : {$i}".PHP_EOL;
  21. file_put_contents( "./yield.log", "gen2".$i, FILE_APPEND );
  22. $rs = yield;
  23. echo "外部发送数据{$rs}".PHP_EOL;
  24. }
  25. }
  26. $gen1 = gen1( $mh, $ch1 );
  27. $gen2 = gen2();
  28. while( true ) {
  29. echo $gen1->current();
  30. echo $gen2->current();
  31. $gen1->send("gen1");
  32. $gen2->send("gen2");
  33. }

我们修改了内容:

将$gen1->next()修改成了$gen1->send(“gen1”)

在function gen1中yield有了返回值,并且将返回值打印出来

这件事情告诉我们:yield和send,是可以双向通信的,同时告诉我们send可以用来恢复原来中断的代码,而且在恢复中断的同时可以携带信息回去。写到这里,你是不是觉得这玩意的可利用价值是不是比原来高点儿了?

我知道,有人肯定叨叨了:“老李,你代码特么写的真是辣鸡啊!你之前保证过了的 —- 只在公司生产环境写辣鸡代码的。可你看看你这辣鸡光环到笼罩都到demo里了,你连demo都不放过了!你怎么说?!”。兄dei,“又不是不能用”。而且我告诉你,上面这点儿curl demo来讲明白yield还是不够的,后面还有两三篇yield呢,照样是烂代码恶心死你,爱看不看。我劝你心放宽,你想想你这么烂的代码都经历了,还有什么不能经历的?

文章最后补个小故事:其实yield是PHP 5.5就已经添加进来了,这个模块的作者叫做Nikita Popov,网络上的名称是Nikic。我们知道PHP7这一代主力是惠新宸,下一代PHP主力就是Nikic了。早在2012年,Nikic就发表了一篇关于PHP yield多任务的文章,链接我贴出来大家共赏一下 —- http://nikic.github.io/2012/12/22/Cooperative-multitasking-using-coroutines-in-PHP.html

上一篇:17. PHP中的yield

  •  

 

  • 0. 介绍
  • 1. php进程daemon化的正确做法
  • 2. php多进程初探—-开篇
  • 3. php多进程初探—-孤儿和僵尸
  • 4. php多进程初探—-信号
  • 5. swoole的进程模型
  • 6. php多进程初探—-利用多进程开发点儿东西
  • 7. php多进程初探—-再次谈daemon进程
  • 8. php多进程初探—-进程间通信二三事
  • 9. PHP Socket初探—-先从一个简单的socket服务器开始
  • 10. PHP socket初探—-关于IO的一些枯燥理论
  • 11. PHP socket初探—-select系统调用
  • 12. PHP socket初探 —- 颤颤抖抖开篇epoll(一)
  • 13. PHP socket初探 —- 硬着头皮继续libevent(二)
  • 14. PHP socket初探 —- 含着泪也要磕完libevent(三)
  • 15. PHP socket初探 —- 一些零碎细节的拾漏补缺(一)
  • 16. swoole的协程是个什么鬼
  • 17. PHP中的yield
  • 18.填坑之PHP的yield和协程在一起的日子里

转载地址:https://www.bookstack.cn/read/advanced-php/18.%E5%A1%AB%E5%9D%91%E4%B9%8BPHP%E7%9A%84yield%E5%92%8C%E5%8D%8F%E7%A8%8B%E5%9C%A8%E4%B8%80%E8%B5%B7%E7%9A%84%E6%97%A5%E5%AD%90%E9%87%8C.md

你可能感兴趣的:(PHP技术)