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

5次阅读

共计 3933 个字符,预计需要花费 10 分钟才能阅读完成。

首先是,这是我第一次把公众号文章复制粘贴到 sf.gg 来。
其次是,很久很久之前,我挖了一个 yield 的一个坑,自己挖的坑自己填,不然迟早会把自己埋掉。
最后是,如果想看之前那个坑,请发送“yield”给文章末尾的公众号,我开通了高大上的自动回复功能,稀罕地不得了!
PS:那篇文章中在最后我犯了一个错误,误下了一个结论:foreach 中不能使用 send 并猜测这是 PHP 的 bug,实际上并不是,真实的原因粗暴简单的理解就是 send 会让生成器继续执行一次导致。这件事情告诉我们:
除了装逼之外,甩锅也是有打脸风险的
那篇坑里,内容和你能在百毒上搜索到的大多数文章都是差不多的,不过我那篇坑标题起得好:《yield 是个什么玩意(上)》,也就是暗示大家还有下篇,所以起标题也是需要一定技术含量的。
我坚信,在座的各位辣鸡在看完上篇坑文后最想说的注定是泰迪熊这句话(这是文化属性,不以各位的意志而转移):

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

虽然文章标题中有“yield 和协程”这样的关键字,但实际上 yield 并不是协程,看起来有不少人直接将 yield 和协程划了等号。yield 的本质是生成器,英文名字叫做 Generator。
yield 只能用在 function 中,但用了 yield 就已经不是传统意义上的 function 了,同时如果你企图在 function 之外的其他地方用 yield,你会被打脸。
yield 的最重要作用就是:自己中断一坨代码的执行,然后主动让出 CPU 控制权给路人甲;然后又能通过一些方式从刚才中断的地方恢复运行。这个就比较屌了,假如你请求了一个费时 10s 的服务器 API,此时是可以让出 CPU 给路人甲。粗暴地说上面的过程就算是协程的基本概念。

多线程和多进程都是操作系统参与的调度,而协程是用户自主实现的调度,协程的关键点实际上是“用户层实现自主调度”,大概有“翻身农奴把歌唱”的意思。
下面我通过一坨代码来体会一把“翻身农奴”,你们感受一下:
<?php
function gen1() {
for($i = 1; $i <= 10; $i++) {
echo “GEN1 : {$i}”.PHP_EOL;
// sleep 没啥意思,主要就是运行时候给你一种切实的调度感,你懂么
// 就是那种“你看!你看!尼玛, 我调度了!卧槽”
sleep(1);
// 这句很关键,表示自己主动让出 CPU,我不下地狱谁下地狱
yield;
}
}
function gen2() {
for($i = 1; $i <= 10; $i++) {
echo “GEN2 : {$i}”.PHP_EOL;
// sleep 没啥意思,主要就是运行时候给你一种切实的调度感,你懂么
// 就是那种“你看!你看!尼玛, 我调度了!卧槽”
sleep(1);
// 这句很关键,表示自己主动让出 CPU,我不下地狱谁下地狱
yield;
}
}
$task1 = gen1();
$task2 = gen2();
while(true) {
// 首先我运行 task1,然后 task1 主动下了地狱
echo $task1->current();
// 这会儿我可以让 task2 介入进来了
echo $task2->current();
// task1 恢复中断
$task1->next();
// task2 恢复中断
$task2->next();
}
上面代码执行结果如下图:

虽然我话都说到这里了,但是肯定还是有人 get 不到“所以,到底发生了什么?”。你要知道,如果 function gen1 和 function gen2 中没有 yield,而是普通函数,你是无法中断其中的 for 循环的,诸如下面这样的代码:
<?php
function gen1() {
for($i = 1; $i <= 10; $i++) {
echo “GEN1 : {$i}”.PHP_EOL;
sleep(1);
}
}
function gen2() {
for($i = 1; $i <= 10; $i++) {
echo “GEN2 : {$i}”.PHP_EOL;
}
}
gen1();
gen2();
// 看这里,看这里,看这里!
// 上面的代码一旦运行,一定是先运行完 gen1 函数中的 for 循环
// 其次才能运行完 gen2 函数中的 for 循环,绝对不会出现
// gen1 和 gen2 交叉运行这种情况

我似乎已然精通了 yield
写到这里后我也开始蹩了,和以往的憋了三天蹦不出来个屁有所不同,我这次蹩出了一个比较典型的应用场景:curl。下面我们基于上面那坨辣鸡代码将 gen1 修改为一个耗时 curl 网络请求,gen2 将向一个文本文件中写内容,我们的目的就是在耗时的 curl 开始后主动让出 CPU,让 gen2 去写文件,以实现 CPU 的最大化利用。
<?php
$ch1 = curl_init();
// 这个地址中的 php,我故意 sleep 了 5 秒钟,然后输出一坨 json
curl_setopt($ch1, CURLOPT_URL, “http://www.selfctrler.com/index.php/test/test1”);
curl_setopt($ch1, CURLOPT_HEADER, 0);
$mh = curl_multi_init();
curl_multi_add_handle($mh, $ch1);
function gen1($mh, $ch1) {
do {
$mrc = curl_multi_exec($mh, $running);
// 请求发出后,让出 cpu
yield;
} while($running > 0);
$ret = curl_multi_getcontent($ch1);
echo $ret.PHP_EOL;
return false;
}
function gen2() {
for ($i = 1; $i <= 10; $i++) {
echo “gen2 : {$i}”.PHP_EOL;
file_put_contents(“./yield.log”, “gen2”.$i, FILE_APPEND);
yield;
}
}
$gen1 = gen1($mh, $ch1);
$gen2 = gen2();
while(true) {
echo $gen1->current();
echo $gen2->current();
$gen1->next();
$gen2->next();
}
上面的代码,运行以后,我们再等待 curl 发起请求的 5 秒钟内,同时可以完成文件写入功能,如果换做平时的 PHP 程序,就只能是先阻塞等待 curl 拿到结果后才能完成文件写入。
文章太长,就像“老太太的裹脚布一样,又臭又长”,所以,最后再对代码做个极小幅度的改动就收尾不写了!
<?php
$ch1 = curl_init();
// 这个地址中的 php,我故意 sleep 了 5 秒钟,然后输出一坨 json
curl_setopt($ch1, CURLOPT_URL, “http://www.selfctrler.com/index.php/test/test1”);
curl_setopt($ch1, CURLOPT_HEADER, 0);
$mh = curl_multi_init();
curl_multi_add_handle($mh, $ch1);
function gen1($mh, $ch1) {
do {
$mrc = curl_multi_exec($mh, $running);
// 请求发出后,让出 cpu
$rs = yield;
echo “ 外部发送数据 {$rs}”.PHP_EOL;
} while($running > 0);
$ret = curl_multi_getcontent($ch1);
echo $ret.PHP_EOL;
return false;
}
function gen2() {
for ($i = 1; $i <= 10; $i++) {
echo “gen2 : {$i}”.PHP_EOL;
file_put_contents(“./yield.log”, “gen2”.$i, FILE_APPEND);
$rs = yield;
echo “ 外部发送数据 {$rs}”.PHP_EOL;
}
}
$gen1 = gen1($mh, $ch1);
$gen2 = gen2();
while(true) {
echo $gen1->current();
echo $gen2->current();
$gen1->send(“gen1”);
$gen2->send(“gen2”);
}
我们修改了内容:
将 $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/1…
最近开了一个微信公众号,所有文章都在这里(手贱弄成服务号了)

正文完
 0