想试试,用纯PHP
代码,不依赖第三方拓展就实现"多线程"么。像 Java
那样使用 setPriority()
影响各个"线程"的被调用概率,使用join()
等待其余线程结束;在sleep
期间让出CPU
占用,到点再回到该"线程";像 Golang
同样,用channel
在协程
之间通讯~php
接上回书,讲完了 yield 基本用法,这篇文章,带你们来实战一下,目标:手把手教会你用 yield 作一个任务调度器,加深对 PHP 生成器 理解。html
建议你们先去看看 以前那篇文章复习下 yield 基础用法。
好,话很少说,开淦~java
在上一讲中,咱们学会了将 function() {...yield...}
就能将一个 函数 变为 “生成器”git
这就是一个简单的任务调度器。代码比较少,直接贴这里了。github
gitee地址: ./simpleYieldScheduler.phpshell
<?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();
运行结果:bootstrap
能够看到咱们用同一个方法和不一样的入参,生成了两个不一样的生成器,用另外一个方法也生成了一个生成器,虽然生成方式不一样,但不影响他们仨一并启动,交替运行,他们的执行顺序肯定(这个脚本运行多少遍都是同一个结果)。segmentfault
咱们来把这个理解透彻,看到yieldFunc($max)
函数,他写了一个循环,循环内带有一个 yield,每当程序运行到这里时,就会跳出当前函数,让出运行时。数组
建立好三个 生成器后,再生成一个 YieldScheduler
对象,把两个 生成器 加入其中,开始运行任务。多线程
在 start()
函数内,就是不断的逐个调用 current
,next
方法,驱使 生成器
运行,每次运行后,会调用 valid
检查 生成器
运行完成与否,完成后,就会从 任务调度器
生成器队列
中踢出该任务。
我这把代码执行顺序伪代码贴一下:
<?php // do 任务调度器 $sum = 0; $re = $gen1->current(); // 进入 gen1 $n = 0; yield $n++; // 跳出 gen1, 获取返回值 赋值给 $re echo '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; } // 跳出 gen2 echo '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; } // 跳出 gen3 echo '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, 获取返回值 赋值给 $re echo '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++)
。 而后呢,才是进入下一个 生成器
。
每一个 生成器(函数)
内的 变量
都有本身的栈空间,不受其余 生成器
影响。 跳出当前生成器,变量的状态依然存在,这个地方就有点像线程的感受,每一个线程也维持者本身的栈空间。因此,你会看到 $i = 0,1,2。。。都打印了3遍。
线程有本身独占的栈内存以及计数器。转载著名出处: sifou
这里打岔讲一下 PHP.net goto.
PHP 中的 goto 有必定限制,目标位置只能位于同一个文件和做用域,也就是说没法跳出一个函数或类方法,也没法跳入到另外一个函数。也没法跳入到任何循环或者 switch 结构中。能够跳出循环或者 switch,一般的用法是用 goto 代替多层的 break。
因此 yield 虽然没有 goto 灵活,可是比 goto 更强大, 能跳 循环,还能跨函数,做用域。
嗯,以上呢就是一个最简单的形态任务调度器,你们先理解透彻了,再继续往下看。
在复杂一点的 任务调度器,就拿鸟哥的转载文章里 在PHP中使用协程实现多任务调度。 的一个任务调度器来说吧,在文章中迭代了2个版本。代码较多,而且代码散落在文章中,我整理后放gitee scheduler了。你们能够clone到本地运行试试。
鸟哥的文章已经讲解得很清楚了,我就不多此一举了,说说我我的感想吧。
文中的代码使用了大量的 闭包,回调,引用。不少地方传递的是 一个个可执行的变量,理解起来有些烧脑。
咱们先看一下Java线程的生命周期, 以及PHP 生成器的状态图。
有不少类似的地方,接下来,咱们就尝试用 PHP yield
实现一个 "类Java的多线程"
调度器。
代码不少,放 gitee 了。
$ php ./YieldBootstrap.php ./YieldSchedulerDemo1.php
这个测试代码,里面用到了priority功能,能够看到 t 须要个周期,t2 须要10个周期,因为t2具备最高的执行优先级,在随机调度过程当中,很快就执行完毕了。最后是 t 和 t3 (t3 须要运行8个周期)最后才执行完毕。
按照 Java 的实现,调用 一个线程的interrupt 方法时,会让该线程,抛出一个异常,而PHP yield 有 throw 方法,我就依葫芦画瓢实现了。
$ php ./YieldBootstrap.php ./YieldSchedulerDemo1.php
代码执行结果以下:
当 YieldThread
对象调用 sleep
方法后,5s内,任务调度输出,就没显示 "线程1" 被执行的输出。
我这代码里的 join,和wait是一个意思。等待线程执行完毕,不过尚未作
join(seconds)
这个功能。
$ php ./YieldBootstrap.php ./YieldSchedulerDemo1.php
执行效果以下
t3 生成器内 调用了t->join() 后,t3 在 t 没执行前完毕以前,就没有被调用过了。
而咱们的 主线程使用 wait(), 等待他们t,t4 俩都执行完毕后才开始 输出本身执行完毕的字符。
整个核心文件就:
能够看到执行命令都是:$ 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
。
这个类多线程调度器,还不那么完善,后续更新会放到 PHP yield thread
文字很少,代码很长,很苦涩,你们下载到本地,多运行,多琢磨琢磨,必定能搞明白 yield
高级用法。欢迎留言,提问。