协程

不需要操作系统参与,创建销毁和切换的成本非常低,遇到io会自动让出cpu执行权,交给其它协程去执行。

协程执行流程

Swoole协程

非协程代码:

<?php
$start = time();
for ($i = 0; $i < 500; $i++) {
    file_get_contents('http://www.easyswoole.com/');
    echo '任务' . $i . '完成' . PHP_EOL;
}
echo '非协程总耗时' . (time() - $start) . 's' . PHP_EOL;

执行结果:

任务495完成
任务496完成
任务497完成
任务498完成
任务499完成
非协程总耗时13s

协程代码:

<?php
$start = time();
for ($i = 0; $i < 500; $i++) {
    Swoole\Coroutine::create(function () use ($i, $start) {
        $client = new Swoole\Coroutine\Http\Client('www.easyswoole.com', 80);
        $client->get('/');
        echo '任务' . $i . '完成' . '耗时' . (time() - $start) . 's' . PHP_EOL;
    });
}

执行结果:

任务389完成耗时1s
任务395完成耗时1s
任务434完成耗时1s
任务477完成耗时1s
任务469完成耗时1s
任务385完成耗时1s
任务498完成耗时1s

可以发现速度相当快,但任务的id,不是顺序执行的,这就是遇到了ioswoole底层自动切换让出cpu执行权。

Channle

用于协程间通讯,支持多生产者协程和多消费者协程。底层自动实现了协程的切换和调度。

执行下面代码:

$start = time();
function task1()
{
    Swoole\Coroutine::sleep(3); // 模拟io阻塞
    echo "发短信\n";
}

function task2()
{
    Swoole\Coroutine::sleep(3); // 模拟io阻塞
    echo "发邮件\n";
}

Swoole\Coroutine::create('task1');
Swoole\Coroutine::create('task2');
echo '总耗时' . (time() - $start) . 's' . PHP_EOL;

执行结果:

总耗时0s
发短信
发邮件

却发现以上代码,先执行的echo '总耗时' . (time() - $start) . 's' . PHP_EOL;

要等待task1task2执行成功后输出,该怎么半呢,这就利用了channel,来实现csp并发编程。

代码:

<?php
Swoole\Coroutine::create(function (){
    $start = time();
    $channel = new Swoole\Coroutine\Channel();
    function task1($channel)
    {
        /** @var Swoole\Coroutine\Channel $channel */
        Swoole\Coroutine::sleep(3); // 模拟io阻塞
        $channel->push("发短信\n");
    }

    function task2($channel)
    {
        /** @var Swoole\Coroutine\Channel $channel */
        Swoole\Coroutine::sleep(3); // 模拟io阻塞
        $channel->push("发邮件\n");
    }

    Swoole\Coroutine::create('task1', $channel);
    Swoole\Coroutine::create('task2', $channel);

    for ($i = 0; $i < 2; $i++) {
        echo $channel->pop();
    }

    echo '总耗时' . (time() - $start) . 's' . PHP_EOL;
});

执行结果:

发短信
发邮件
总耗时3s

可以看到耗时3s,但我们在增加一个任务,for里面的$i就要修改,使得我们的代码非常繁琐,所以就有了WaitGroup

channel可以实现协程通信,依赖管理,协程同步。

实现连接池功能可以看我之前的文章,传送门

WaitGroup

基于Channel实现的Golangsync.WaitGrup功能。

方法:

  • add 方法增加计数
  • done 表示任务已完成
  • wait 等待所有任务完成恢复当前协程的执行
  • WaitGroup 对象可以复用,adddonewait 之后可以再次使用

代码:

<?php
Swoole\Coroutine::create(function () {

    $start = time();
    $waitGroup = new Swoole\Coroutine\WaitGroup();
    function task1($waitGroup)
    {
        /** @var WaitGroup $waitGroup */
        Swoole\Coroutine::sleep(3); // 模拟io阻塞
        echo "发短信\n";
        $waitGroup->done();;
    }

    function task2($waitGroup)
    {
        /** @var WaitGroup $waitGroup */
        Swoole\Coroutine::sleep(3); // 模拟io阻塞
        echo "发邮件\n";
        $waitGroup->done();

    }

    $waitGroup->add();
    Swoole\Coroutine::create('task1', $waitGroup);

    $waitGroup->add();
    Swoole\Coroutine::create('task2', $waitGroup);

    $waitGroup->wait();

    echo '总耗时' . (time() - $start) . 's' . PHP_EOL;
});

执行结果跟之前一样,也是好时3s,但是不是更简单了呢。

Context

协程原有的异步逻辑同步化,但是在协程切换是隐式发生的,所有协程切换的前后不能保证全局遍历及static变量的一致性。

context用协程id做隔离,来保存上下文内容。

代码复现:

<?php
class Email
{
    static $email = null;
}

Swoole\Coroutine::create(function () {

    function task1($email)
    {
        Email::$email = $email;
        Swoole\Coroutine::sleep(3); // 模拟io阻塞
        echo "发邮件:" . Email::$email . PHP_EOL;
    }

    function task2($email)
    {
        Email::$email = $email;
        echo "发邮件:" . Email::$email . PHP_EOL;

    }

    Swoole\Coroutine::create('task1', '975975398@gmail.com');

    Swoole\Coroutine::create('task2', 'gaobinzhan@gmail.com');

});

从感觉啥觉得会输出两个邮箱地址,但其实:

发邮件:gaobinzhan@gmail.com
发邮件:gaobinzhan@gmail.com

这就是变量生命周期,需要注意,我们可以封装一个类来保存上下文。

<?php
class Context
{
    /**
     * [
     *    'cid' => [  // 就是协程的id
     *        'key' => 'value' // 保存的全局变量的信息
     *    ]
     * ]
     * @var [type]
     */
    public static $pool = [];

    static function get($key)
    {
        $cid = Swoole\Coroutine::getuid();// 获取当前运行的协程id
        if ($cid < 0) {
            return null;
        }
        if (isset(self::$pool[$cid][$key])) {
            return self::$pool[$cid][$key];
        }
        return null;
    }

    static function put($key, $item)
    {
        $cid = Swoole\Coroutine::getuid();// 获取当前运行的协程id
        if ($cid > 0) {
            self::$pool[$cid][$key] = $item;
        }
    }

    static function delete($key = null)
    {
        $cid = Swoole\Coroutine::getuid();
        if ($cid > 0) {
            if ($key) {
                unset(self::$pool[$cid][$key]);
            } else {
                unset(self::$pool[$cid]);
            }
        }
        var_dump(self::$pool);
    }
}

运行以下代码:

<?php
Swoole\Coroutine::create(function () {

    function task1($email)
    {
        Context::put('email',$email);
        Swoole\Coroutine::sleep(3); // 模拟io阻塞
        echo "发邮件:" . Context::get('email') . PHP_EOL;
    }

    function task2($email)
    {
        Context::put('email',$email);
        echo "发邮件:" . Context::get('email') . PHP_EOL;
    }

    Swoole\Coroutine::create('task1', '975975398@gmail.com');

    Swoole\Coroutine::create('task2', 'gaobinzhan@gmail.com');

    var_dump(Context::$pool);
});

运行结果:

发邮件:gaobinzhan@gmail.com
array(2) {
  [2]=>
  array(1) {
    ["email"]=>
    string(19) "975975398@gmail.com"
  }
  [3]=>
  array(1) {
    ["email"]=>
    string(20) "gaobinzhan@gmail.com"
  }
}
发邮件:975975398@gmail.com

可以看到,两个邮箱都输出成功了,但是我们的变量没有销毁,如何销毁呢,Swoole提供了defer方法,在协程关闭之前会调用defer

<?php
Swoole\Coroutine::create(function () {

    function task1($email)
    {
        Context::put('email',$email);
        Swoole\Coroutine::sleep(3); // 模拟io阻塞
        echo "发邮件:" . Context::get('email') . PHP_EOL;
        Swoole\Coroutine::defer(function (){
            Context::delete();
        });
    }

    function task2($email)
    {
        Context::put('email',$email);
        echo "发邮件:" . Context::get('email') . PHP_EOL;
        Swoole\Coroutine::defer(function (){
            Context::delete();
        });
    }

    Swoole\Coroutine::create('task1', '975975398@gmail.com');

    Swoole\Coroutine::create('task2', 'gaobinzhan@gmail.com');


});

运行结果:

发邮件:gaobinzhan@gmail.com
array(1) {
  [2]=>
  array(1) {
    ["email"]=>
    string(19) "975975398@gmail.com"
  }
}
发邮件:975975398@gmail.com
array(0) {
}

可以发现到最后为空,已经被清空掉了。

是不是觉得这样写很麻烦,以及不确定在什么时候销毁,然而Swoole提供的Context可以让协程退出后上下文自动清理 (如无其它协程或全局变量引用)。

代码实现:

<?php
Swoole\Coroutine::create(function () {

    function task1($email)
    {
        $context = Swoole\Coroutine::getContext();
        $context->email = $email;
        Swoole\Coroutine::sleep(3); // 模拟io阻塞
        echo "发邮件:" . $context->email . PHP_EOL;
    }

    function task2($email)
    {
        $context = Swoole\Coroutine::getContext();
        $context->email = $email;
        echo "发邮件:" . $context->email . PHP_EOL;
    }

    Swoole\Coroutine::create('task1', '975975398@gmail.com');

    Swoole\Coroutine::create('task2', 'gaobinzhan@gmail.com');
    
});

最后修改:2020 年 05 月 16 日 04 : 01 PM