您的位置 首页 php

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();  

运行结果:

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

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

这里打岔讲一下 PHP.net goto .

PHP 中的 goto 有一定限制,目标位置只能位于同一个文件和作用域,也就是说无法跳出一个函数或类方法,也无法跳入到另一个函数。也无法跳入到任何循环或者 switch 结构中。可以跳出循环或者 switch,通常的用法是用 goto 代替多层的 break。

所以 yield 虽然没有 goto 灵活,但是比 goto 更强大, 能跳 循环,还能跨函数,作用域。

嗯,以上呢就是一个最简单的形态任务调度器,大家先理解透彻了,再继续往下看。

复杂一点的 任务调度器

在复杂一点的 任务调度器,就拿鸟哥的转载文章里 在PHP中使用协程实现多任务调度 。 的一个任务调度器来讲吧,在文章中迭代了2个版本。代码较多,并且代码散落在文章中,我整理后放 gitee scheduler 了。大家可以clone到本地运行试试。

鸟哥的文章已经讲解得很清楚了,我就不画蛇添足了,说说我个人感想吧。

文中的代码使用了大量的 闭包,回调,引用。很多地方传递的是 一个个可执行的变量,理解起来有些烧脑。

类似多线程那样的任务调度器

我们先看一下Java线程的生命周期, 以及PHP 生成器的状态图。

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

代码很多,放 gitee 了。

讲解

第一个 Demo , priority

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

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

第二个Demo, interrupt,sleep

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

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

代码执行结果如下:

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

第三个Demo, join,wait

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

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

执行效果如下

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 。

文章来源:智云一二三科技

文章标题:PHP yield 协程实战—“多线程”任务调度器

文章地址:https://www.zhihuclub.com/79080.shtml

关于作者: 智云科技

热门文章

网站地图