php中多线程中run方法没有输出echo_PHP yield 协程实战—“多线程”任务调度器

想试试,用纯 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

可以看到我们用同一个方法和不同的入参,生成了两个不同的生成器,用另一个方法也生成了一个生成器,虽然生成方式不同,但不影响他们仨一并启动,交替运行,他们的执行顺序确定(这个脚本运行多少遍都是同一个结果)。

我们来把这个理解透彻,看到 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

每个 生成器(函数) 内的 变量 都有自己的栈空间,不受其他 生成器 影响。 跳出当前生成器,变量的状态依然存在,这个地方就有点像线程的感觉,每个线程也维持着自己的栈空间。所以,你会看到 $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
900ced10dfcd9d6e3189d786d1f09ad2.png

有很多相似的地方,接下来,我们就尝试用 PHP yield 实现一个 "类Java的多线程" 调度器。

代码很多,放 gitee 了。

讲解

第一个Demo, priority
$ php ./YieldBootstrap.php ./YieldSchedulerDemo1.php
df20c50f087e0dec6ed8c4d920bef2e0.png

这个测试代码,里面用到了priority功能,可以看到 t 需要个周期,t2 需要10个周期,由于t2具有最高的执行优先级,在随机调度过程中,很快就执行完毕了。最后是 t 和 t3 (t3 需要运行8个周期)最后才执行完毕。

第二个Demo, interrupt,sleep

按照 Java 的实现,调用 一个线程的interrupt 方法时,会让该线程,抛出一个异常,而PHP yield 有 throw 方法,我就依葫芦画瓢实现了。

$ php ./YieldBootstrap.php ./YieldSchedulerDemo1.php

代码执行结果如下:

340e944c7f5b0467003586375957dba0.png

当 YieldThread 对象调用 sleep 方法后,5s内,任务调度输出,就没显示 "线程1" 被执行的输出。

第三个Demo, join,wait

我这代码里的 join,和wait是一个意思。等待线程执行完毕,不过还没有做 join(seconds) 这个功能。

$ php ./YieldBootstrap.php ./YieldSchedulerDemo1.php

执行效果如下

fbb1ed56ca9483f73990b8d8c48be173.png

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 。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值