什么是协程:协程可以简单理解为线程,只不过这个线程是用户态的,不需要操作系统参与,创建销毁和切换的成本非常低,和线程不同的是协程没法利用多核 cpu 的,想利用多核 cpu 需要依赖 Swoole 的多进程模型。

父子协程优先级: 优先执行子协程 (即 go() 里面的逻辑),直到发生协程 yield(co::sleep 处),然后协程调度到外层协程

实验环境

php version = 7.2.4

swoole version = 4.5.2


实验1:协程容器中的协程代码运行顺序

实验代码:

<?php

echo "main start\n";                                                        //1. 最早执行,输出main start
Co\run(function () {                                                        //2. Co\run执行完之后,才能够走下面的步骤
    //coro 1 run创建的最外层
    echo "coro " . co::getcid() . " start\n";                               //2-1. 2中最早执行,输出coro 1 start

    go(function () {
        //coro 2 go创建的coro1里面,父进程是coro 1
        echo "coro " . co::getcid() . " start\n";                           //2-2. 优先进入协程里面,但是go函数是异步的,并不是必须全部执行完成后才接着往下走,输出coro 2 start
        co::sleep(.2);                                                      //2-3. 子进程挂起后,执行当前协程的外层代码
        echo "coro " . co::getcid() . " end\n";                             //2-10(实际2-12). 被resume执行,但是sleep 0.2秒后才执行,期间另外两个被resume的进程已经执行完毕
                                                                            //     run中所有父子进程都已执行完毕,可以顺序执行run外的代码了
    });


    go(function () {
        //coro 3 go创建的coro1里面,coro2进程已存在,那么递增为3。父进程是coro 1
        echo "coro " . co::getcid() . " start\n";                           //2-4. 优先进入协程里面,输出coro 3 start
        co::sleep(.1);                                                      //2-5. 子进程挂起后,执行当前协程的外层代码
        echo "coro " . co::getcid() . " end\n";                             //2-10. 被resume执行,但是coro 2 sleep 0.2秒,那么执行的比coro2快,且比coro1外层先执行,同样sleep 0.1,所以它会更快执行,输出coro 3 end
                                                                            //      如果sleep 0.2秒,那么下面的coro 1 sleep 0.1秒就会先执行;它跟coro 2 sleep时间一样,但是因为coro2 先执行,所以它会比coro2慢
    });

    //!不能够在run里面再使用run
    //    Co\run(function(){
    //        echo 'run '.co::getcid().' start'.PHP_EOL;
    //       co::sleep(.2);
    //       echo 'run '.co::getcid().' end'.PHP_EOL;
    //    });


    go(function () {
        //coro 4 go创建的coro1里面,coro2、3进程已存在,那么递增为4。父进程是coro 1
        echo "coro " . co::getcid() . " start\n";                           //2-6. 进入协程,输出coro 4 start
        echo "coro " . co::getcid() . " end\n";                             //2-7. 没有被挂起,直接往下执行,输出去coro 4 end
    });


    echo "coro " . co::getcid() . " do not wait children coroutine\n";      //2-8. 子进程被挂起,接着执行它,输出coro 1 do not wait children coroutine
    co::sleep(.1);                                                          //2-9. 挂起,让出当前协程,但是在run中,无法继续执行外面的命令,只能够resume父子所有进程,自己和子进程同时进行
    echo "coro " . co::getcid() . " end\n";                                 //2-10(实际2-11). 被resume执行,但是sleep 0.1秒后才执行
});
echo "end\n";                                                               //3. run执行完后才可执行

执行结果:
实验1结果

实验2:无协程容器下的协程代码运行顺序

swoole官方文档 中说道:“所有的协程必须在协程容器里面创建Swoole 程序启动的时候大部分情况会自动创建协程容器"。那么在不自主创建协程容器的情况下,swoole程序创建的协程容器应是将整个代码文件中的代码都归入其回调函数参数中。

实验代码:

<?php

echo "main start\n";                                                        //1. 最早执行,输出main start
Co\run(function () {                                                        //2. Co\run执行完之后,才能够走下面的步骤
    //coro 1 run创建的最外层
    echo "coro " . co::getcid() . " start\n";                               //2-1. 2中最早执行,输出coro 1 start

    go(function () {
        //coro 2 go创建的coro1里面,父进程是coro 1
        echo "coro " . co::getcid() . " start\n";                           //2-2. 优先进入协程里面,但是go函数是异步的,并不是必须全部执行完成后才接着往下走,输出coro 2 start
        co::sleep(.2);                                                      //2-3. 子进程挂起后,执行当前协程的外层代码
        echo "coro " . co::getcid() . " end\n";                             //2-10(实际2-12). 被resume执行,但是sleep 0.2秒后才执行,期间另外两个被resume的进程已经执行完毕
                                                                            //     run中所有父子进程都已执行完毕,可以顺序执行run外的代码了
    });


    go(function () {
        //coro 3 go创建的coro1里面,coro2进程已存在,那么递增为3。父进程是coro 1
        echo "coro " . co::getcid() . " start\n";                           //2-4. 优先进入协程里面,输出coro 3 start
        co::sleep(.1);                                                      //2-5. 子进程挂起后,执行当前协程的外层代码
        echo "coro " . co::getcid() . " end\n";                             //2-10. 被resume执行,但是coro 2 sleep 0.2秒,那么执行的比coro2快,且比coro1外层先执行,同样sleep 0.1,所以它会更快执行,输出coro 3 end
                                                                            //      如果sleep 0.2秒,那么下面的coro 1 sleep 0.1秒就会先执行;它跟coro 2 sleep时间一样,但是因为coro2 先执行,所以它会比coro2慢
    });

    //!不能够在run里面再使用run
    //    Co\run(function(){
    //        echo 'run '.co::getcid().' start'.PHP_EOL;
    //       co::sleep(.2);
    //       echo 'run '.co::getcid().' end'.PHP_EOL;
    //    });


    go(function () {
        //coro 4 go创建的coro1里面,coro2、3进程已存在,那么递增为4。父进程是coro 1
        echo "coro " . co::getcid() . " start\n";                           //2-6. 进入协程,输出coro 4 start
        echo "coro " . co::getcid() . " end\n";                             //2-7. 没有被挂起,直接往下执行,输出去coro 4 end
    });


    echo "coro " . co::getcid() . " do not wait children coroutine\n";      //2-8. 子进程被挂起,接着执行它,输出coro 1 do not wait children coroutine
    co::sleep(.1);                                                          //2-9. 挂起,让出当前协程,但是在run中,无法继续执行外面的命令,只能够resume父子所有进程,自己和子进程同时进行
    echo "coro " . co::getcid() . " end\n";                                 //2-10(实际2-11). 被resume执行,但是sleep 0.1秒后才执行
});
echo "end\n";                                                               //3. run执行完后才可执行

执行结果:
实验2结果

实验3:协程代码受时间差低影响的运行顺序

实验1代码下,调整co::sleep时间,将时间差的影响降到最低。

实验代码:

<?php

$saveResArr = [];

//这里顺便使用swoole table进行共享内存测试,通常建议使用redis等缓存来保存数据
$table = new Swoole\Table(1024);
$table->column('content', Swoole\Table::TYPE_STRING, 1024);
$table->create();

for ($i = 0; $i < 10000; $i++) {
    $table->set('test', ['content' => 'main start']);                                           //1. 最早执行,输出main start
    Co\run(function () use ($table) {                                                           //2. Co\run执行完之后,才能够走下面的步骤
        $data = $table->get('test')['content'];
        $table->set('test', ['content' => $data . "|coro 1 start"]);                            //2-1. 2中最早执行,输出coro 1 start

        go(function () use ($table) {
            $data = $table->get('test')['content'];
            $table->set('test', ['content' => $data . "|coro 2 start"]);                        //2-2. 优先进入协程里面,但是go函数是异步的,并不是必须全部执行完成后才接着往下走,输出coro 2 start
            co::sleep(.001);                                                                    //2-3. 子进程挂起后,执行当前协程的外层代码
            $data = $table->get('test')['content'];
            $table->set('test', ['content' => $data . "|coro 2 end"]);                          //2-10 被resume执行,但是sleep 0.001秒后才执行,原则上最先执行
        });


        go(function () use ($table) {
            $data = $table->get('test')['content'];
            $table->set('test', ['content' => $data . "|coro 3 start"]);                        //2-4. 优先进入协程里面,输出coro 3 start
            co::sleep(.001);                                                                    //2-5. 子进程挂起后,执行当前协程的外层代码
            $data = $table->get('test')['content'];
            $table->set('test', ['content' => $data . "|coro 3 end"]);                          //2-10. 被resume执行,原则上比coro1外层先执行,比coro2慢
        });

        go(function () use ($table) {
            $data = $table->get('test')['content'];
            $table->set('test', ['content' => $data . "|coro 4 start"]);                        //2-6. 进入协程,输出coro 4 start

            $data = $table->get('test')['content'];
            $table->set('test', ['content' => $data . "|coro 4 end"]);                          //2-7. 没有被挂起,直接往下执行,输出去coro 4 end
        });


        $data = $table->get('test')['content'];
        $table->set('test', ['content' => $data . "|coro 1 do not wait children coroutine"]);   //2-8. 子进程被挂起,接着执行它,输出coro 1 do not wait children coroutine

        co::sleep(.001);                                                                        //2-9. 挂起,让出当前协程,但是在run中,无法继续执行外面的命令,只能够resume父子所有进程,自己和子进程同时进行
        $data = $table->get('test')['content'];
        $table->set('test', ['content' => $data . "|coro 1 end"]);                              //2-10. 被resume执行,原则上最晚执行

    });

    $data = $table->get('test')['content'];
    $table->set('test', ['content' => $data . "|end"]);                                         //3. run执行完后才可执行

    //保存数据
    $data = $table->get('test')['content'];
    if (!in_array($data, $saveResArr)) {
        $saveResArr[] = $data;
    }

    //释放数据
    $table->del('test');
}


//输出结果
foreach ($saveResArr as $saveRes) {
    foreach (explode('|', $saveRes) as $row) {
        echo $row . PHP_EOL;
    }
    echo '----------------------' . PHP_EOL;
}

执行结果:
实验3结果

总结

由于底层会优先执行子协程的代码,因此只有子协程挂起时,Coroutine::create(go) 才会返回,继续执行当前协程的代码。

  • go如果挂起,就会接着往下面走程序,当程序不能够往下执行,才会resume
  • 协程容器Co\run可当成是同步中的执行的一个函数,只有执行完该函数后才可继续执行;
  • 协程容器中不能再创建协程容器
  • 如果sleep时间过短(如0.001毫秒),那么难以保证时间差内代码是否已经完成,也就是同时被resume的子进程顺序执行可能存在变化
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐