引
想试试,用纯 PHP 代码,不依赖第三方拓展就实现"多线程"么。像 Java 那样使用 setPriority() 影响各个"线程"的被调用几率,使用 join() 等待其他线程结束;在 sleep 期间让出 CPU 占用,到点再回到该"线程";像 Golang 一样,用 channel 在 协程 之间通信~
续
接上回书,讲完了 yield 基本用法,这篇文章,带大家来实战一下,目标:手把手教会你用 yield 做一个任务调度器,加深对 PHP 生成器 理解。
建议大家先去看看 之前那篇文章复习下 yield 基础用法。
好,话不多说,开淦~
点睛
在上一讲中,我们学会了将 function() {...yield...} 就能将一个 函数 变为 “生成器”
一个简单任务调度器
这就是一个简单的任务调度器。代码比较少,直接贴这里了。
gitee地址: ./simpleYieldScheduler.php
<?php /** * Class YieldScheduler */Class YieldScheduler{ /** * @var array $gens */ public $gens = array(); /** * 新增任务到 调度器 * * @param Generator $gen * @param null $key * * @return $this */ public function add($gen, $key = null) { if (null === $key) { $this->gens[] = $gen; } else { $this->gens[$key] = $gen; } return $this; } /** * 开始 */ public function start() { $keepRun = true; /** * @var Generator $gen */ $gen = null; do { // 循环调度任务 foreach ($this->gens as $id => $gen) { $re = $gen->current(); echo 'generator id: ' . $id . ' run, get current re : ' . $re . PHP_EOL; $gen->next(); } // 检查任务是否已完成 foreach ($this->gens as $id => $gen) { $check = $gen->valid(); if (!$check) { // 已执行完毕的任务就可以踢出任务调度队列了 unset($this->gens[$id]); } } // 调度器是否完成所有任务 if (0 >= count($this->gens)) { $keepRun = false; } } while ($keepRun); }}function yieldFunc($max = 10){ for($i = 0; $i < $max; $i ++) { (yield $i); } return $i;}$gen1 = yieldFunc(3);$gen2 = yieldFunc(5);$scheduler = new YieldScheduler();$scheduler->add($gen1)->add($gen2);$scheduler->start();
运行结果:
![017043607b729ca9b4d179b7270dd241.png](https://i-blog.csdnimg.cn/blog_migrate/4df0e8e2bf98ca5ddd39140abb788aa4.jpeg)
可以看到我们用同一个方法和不同的入参,生成了两个不同的生成器,用另一个方法也生成了一个生成器,虽然生成方式不同,但不影响他们仨一并启动,交替运行,他们的执行顺序确定(这个脚本运行多少遍都是同一个结果)。
我们来把这个理解透彻,看到 yieldFunc($max) 函数,他写了一个循环,循环内带有一个 yield,每当程序运行到这里时,就会跳出当前函数,让出运行时。
创建好三个 生成器后,再生成一个 YieldScheduler 对象,把两个 生成器 加入其中,开始运行任务。
在 start() 函数内,就是不断的逐个调用 current , next 方法,驱使 生成器 运行,每次运行后,会调用 valid 检查 生成器 运行完成与否,完成后,就会从 任务调度器 生成器队列 中踢出该任务。
运行伪代码
我这把代码执行顺序伪代码贴一下:
<?php // do 任务调度器$sum = 0;$re = $gen1->current(); // 进入 gen1 $n = 0; yield $n++; // 跳出 gen1, 获取返回值 赋值给 $reecho 'generator id: ' . $id . ' run, get current re : ' . $re . PHP_EOL;$gen1->send($sum++) // sum = 1 // 进入 gen1 $receive = yield; echo 'get scheduler sent : ' . $receive . PHP_EOL; $n++; // 跳出 gen1// 任务调度器检查任务是否完成if (!$gen1->valid()) { unset($gen1);}if (empty($gens)) { break;}// 任务调度器进入第二个循环// 开始调度 第二个 生成器$re = $gen2->current(); // 进入 gen2 , $i = 0; if ($i < $max) { yield $i; } // 跳出 gen2echo 'generator id: ' . $id . ' run, get current re : ' . $re . PHP_EOL;$gen2->send($sum++) // sum = 2 // 进入 gen2 $get = yield; echo 'get scheduler sent : ' . $get . PHP_EOL; $i++; if ($i < $max){ return $i; } // 跳出 gen2// 任务调度器检查任务是否完成if (!$gen2->valid()) { unset($gen2);}if (empty($gens)) { break;}// 任务调度器进入第三个循环// 开始调度 第三个 生成器$re = $gen3->current(); // 进入 gen3, 这是第三个生成器,此 $i 不是 gen2 的 $i,所以 $i 从 0开始 $i = 0; if ($i < $max) { yield $i; } // 跳出 gen3echo 'generator id: ' . $id . ' run, get current re : ' . $re . PHP_EOL;$gen2->send($sum++) // sum = 3 // 进入 gen3 $get = yield; echo 'get scheduler sent : ' . $get . PHP_EOL; $i++; if ($i < $max){ return $i; } // 跳出 gen3// 任务调度器检查任务是否完成if (!$gen3->valid()) { unset($gen3);}if (empty($gens)) { break;}// 任务调度器进入第四个循环// 又开始调度 第1个 生成器$re = $gen1->current(); // 进入 gen1 yield $n; // $n = 1, 这里 $n++ 在第一次调度时,已完成? // 跳出 gen1, 获取返回值 赋值给 $reecho 'generator id: ' . $id . ' run, get current re : ' . $re . PHP_EOL;$gen1->send($sum++) // sum = 4 // 进入 gen1 $receive = yield; echo 'get scheduler sent : ' . $receive . PHP_EOL; $n++; // 跳出 gen1// 任务调度器检查任务是否完成if (!$gen1->valid()) { unset($gen1);}if (empty($gens)) { break;}
看这伪代码的执行顺序,你想到了什么呢? goto !, PHP 也支持 goto 语法的,为了代码的阅读,易于维护,一般很少用它。
代码执行到 yiel d的右侧就跳出,这里有个细节一定要扣一下,那就是 yield 右侧表达式,或者函数执行完,才会跳出当前 生成器(并不是指定到 yield 这一行代码时,退出)。这个细节,你可以从 yieldFunc 和 myPrint 调用后的,命令行输出可以看到。在 任务调度器 第4个循环调度时,调用 send() 方法后, 生成器 内不仅执行完毕了 echo 'get scheduler sent : ' . $receive . PHP_EOL; , 还执行了 myPrint($n++) 。 然后呢,才是进入下一个 生成器 。
![abcc406d62eb64f5e0b9df5a92ef43f8.png](https://i-blog.csdnimg.cn/blog_migrate/71aa24856ad02f5fa43b676659e5743d.jpeg)
每个 生成器(函数) 内的 变量 都有自己的栈空间,不受其他 生成器 影响。 跳出当前生成器,变量的状态依然存在,这个地方就有点像线程的感觉,每个线程也维持着自己的栈空间。所以,你会看到 $i = 0,1,2。。。都打印了3遍。
线程有自己独占的栈内存以及计数器。
转载著名出处:sifou
PHP 的 goto
这里打岔讲一下 PHP.net goto .
PHP 中的 goto 有一定限制,目标位置只能位于同一个文件和作用域,也就是说无法跳出一个函数或类方法,也无法跳入到另一个函数。也无法跳入到任何循环或者 switch 结构中。可以跳出循环或者 switch,通常的用法是用 goto 代替多层的 break。
所以 yield 虽然没有 goto 灵活,但是比 goto 更强大, 能跳 循环,还能跨函数,作用域。
嗯,以上呢就是一个最简单的形态任务调度器,大家先理解透彻了,再继续往下看。
复杂一点的 任务调度器
在复杂一点的 任务调度器,就拿鸟哥的转载文章里 在PHP中使用协程实现多任务调度 。 的一个任务调度器来讲吧,在文章中迭代了2个版本。代码较多,并且代码散落在文章中,我整理后放 gitee scheduler 了。大家可以clone到本地运行试试。
鸟哥的文章已经讲解得很清楚了,我就不画蛇添足了,说说我个人感想吧。
文中的代码使用了大量的 闭包,回调,引用。很多地方传递的是 一个个可执行的变量,理解起来有些烧脑。
类似多线程那样的任务调度器
我们先看一下Java线程的生命周期, 以及PHP 生成器的状态图。
![d86798b42f9b179139d15a88468d67fb.png](https://i-blog.csdnimg.cn/blog_migrate/eedf191476377dc4c5dfb20b38717dd8.jpeg)
![900ced10dfcd9d6e3189d786d1f09ad2.png](https://i-blog.csdnimg.cn/blog_migrate/2c34c3ed2636d8def6303777154f3657.jpeg)
有很多相似的地方,接下来,我们就尝试用 PHP yield 实现一个 "类Java的多线程" 调度器。
代码很多,放 gitee 了。
讲解
第一个Demo, priority
$ php ./YieldBootstrap.php ./YieldSchedulerDemo1.php
![df20c50f087e0dec6ed8c4d920bef2e0.png](https://i-blog.csdnimg.cn/blog_migrate/8cb1f72629b820fb5ea484d142610040.jpeg)
这个测试代码,里面用到了priority功能,可以看到 t 需要个周期,t2 需要10个周期,由于t2具有最高的执行优先级,在随机调度过程中,很快就执行完毕了。最后是 t 和 t3 (t3 需要运行8个周期)最后才执行完毕。
第二个Demo, interrupt,sleep
按照 Java 的实现,调用 一个线程的interrupt 方法时,会让该线程,抛出一个异常,而PHP yield 有 throw 方法,我就依葫芦画瓢实现了。
$ php ./YieldBootstrap.php ./YieldSchedulerDemo1.php
代码执行结果如下:
![340e944c7f5b0467003586375957dba0.png](https://i-blog.csdnimg.cn/blog_migrate/24e728e066c94d7cc57a121f3322ef89.jpeg)
当 YieldThread 对象调用 sleep 方法后,5s内,任务调度输出,就没显示 "线程1" 被执行的输出。
第三个Demo, join,wait
我这代码里的 join,和wait是一个意思。等待线程执行完毕,不过还没有做 join(seconds) 这个功能。
$ php ./YieldBootstrap.php ./YieldSchedulerDemo1.php
执行效果如下
![fbb1ed56ca9483f73990b8d8c48be173.png](https://i-blog.csdnimg.cn/blog_migrate/8abc1065851569baeb331f0b12226219.jpeg)
t3 生成器内 调用了t->join() 后,t3 在 t 没执行前完毕之前,就没有被调用过了。
而我们的 主线程使用 wait(), 等待他们t,t4 俩都执行完毕后才开始 输出自己执行完毕的字符。
原理
整个核心文件就:
- InterruptedException.php
- MainYieldTread.php
- YieldBootstrap.php
- YieldThread.php
- YieldThreadScheduler.php
可以看到执行命令都是: $ php ./YieldBootstrap.php ./YieldSchedulerDemo1.php。php 调用 YieldBootstrap.php 程序,自定义的代码(demo代码),是作为参数传入。在 bootstrap 中,会对主程序做一个包装—— MainYieldThread.php 包裹主 生成器 。而 用户自定义的线程是继承自 YieldThread.php , 主线程,自线程,都继承自 YieldThread , 都放入到 YieldThreadScheduler.php 中,统一调度,这样就实现了,线程切换。
这个"线程"的接口设计是照搬 Java 的,原理实现呢,就按照 Java-Thread 生命周期图,以及 PHP-yield 的活动状态图推演实现的。任务调度,优先级采用了轮盘,加随机数实现的随机调度。 join 、 wait 是通过一个数组记录各个线程之间的依赖关系来判断,当先线程是否 ready 。