上一篇 swoole4.0协程初探中通过并发请求淘宝和百度的首页的简单示例,大概了解了一下swoole的协程是如何工作的,示例中我们是在http请求返回后,分别做了相关的逻辑处理,就有人提到,能否有类似于golang的sync.WaitGroup,等所有的协程数据返回了再做统一处理?

答案是肯定的,必需能支持,接下来出场的就是swoole的另一个重要特性:channel,下面我们就来通过代码了解一下,如何通过channel实现WaitGroup功能

<?php
class waitgroup
{

    private $count = 0;
    private $chan;

    /**
     * waitgroup constructor.
     * @desc 初始化一个channel
     */
    public function __construct()
    {
        $this->chan = new chan;
    }

    /**
     * @desc 计数+1
     * @调用时机:在起一个协程前
     */
    public function add()
    {
        $this->count++;
    }

    /**
     * @desc 协程处理完成时调用
     */
    public function done()
    {

        $this->chan->push(true);
    }

    /**
     * @desc 堵塞的等待所有的协程处理完成
     */
    public function wait()
    {
        for ($i = 0; $i < $this->count; $i++) {
            //调用pop方法时,如果没有数据,此协程会挂起
            //当往chan中push数据后,协程会被恢复
            $this->chan->pop();
        }
    }

}

这么简单的一串代码就实现了WaitGroup功能了,那之前的http代码可以这样改造:

<?php
require_once "waitgroup.php";

use Swoole\Coroutine\Http\Client;

//此方法记录执行时间
function timediff($time)
{
    return microtime(true) - $time;
}

//创建http server
$http = new Swoole\Http\Server("0.0.0.0", 9501);
$http->set([
    //"daemonize" => true,
    "worker_num" => 1,
]);
$http->on('request', function ($request, $response) {

    //浏览器会自动发起这个请求,这也是很多人碰到的一个问题:
    //为什么我浏览器打开网站,收到了两个请求?
    if ($request->server['path_info'] == '/favicon.ico') {
        $response->end('');
        return;
    }

    $time = microtime(true);
    $response->header("content-type", "text/html; charset=UTF-8");
    //定义一个数组,用于存储结果,方便统一输出
    $result = [];
    $result[] = "1. 接受请求,此处被执行, 第" . __LINE__ . "行, 时间" . $time . "<br/>";


    $wg = new waitgroup();


    //加入wait计数
    $wg->add();
    //启动第一个协程
    go(function () use ($response, $wg, &$result) {
        $time = microtime(true);
        $result[] = "2. 进入第一个协程,发起http请求taobao, 第" . __LINE__ . "行, 时间:" . $time . "<br/>";

        //启动一个协程客户端client,请求淘宝首页
        $cli = new Client('www.taobao.com', 443, true);
        $cli->setHeaders([
            'Host' => "www.taobao.com",
            "User-Agent" => 'Chrome/49.0.2587.3',
            'Accept' => 'text/html,application/xhtml+xml,application/xml',
            'Accept-Encoding' => 'gzip',
        ]);
        $cli->set(['timeout' => 1]);
        //调用get方法,协程挂起,
        $cli->get('/index.php');
        //会等待i/o数据返回,执行wg的done方法,表示协程数据已返回
        $result[] = "7. get回taobao数据,唤起协程,此处被执行, 第" . __LINE__ . "行, 执行时间" . timediff($time) . "<br/>";
        $cli->close();

        //放在协程的最后执行
        $wg->done();
    });
    //上面get挂起协程后,后立马执行这一行
    $result[] = "3 cli->get时挂起协程了,此处被执行,不会被阻塞, 第" . __LINE__ . "行, 时间:" . microtime(true) . "<br/>";

    //加入wait计数
    $wg->add();
    //启动第二个协程
    go(function () use ($response, $wg, &$result) {
        $time = microtime(true);
        $result[] = "4. 进入第二个协程,发起http请求baidu, 第" . __LINE__ . "行, 时间:" . $time . "<br/>";
        //启动一个协程客户端client,请求百度首页
        $cli = new Client('www.baidu.com', 443, true);
        $cli->setHeaders([
            'Host' => "www.baidu.com",
            "User-Agent" => 'Chrome/49.0.2587.3',
            'Accept' => 'text/html,application/xhtml+xml,application/xml',
            'Accept-Encoding' => 'gzip',
        ]);
        $cli->set(['timeout' => 1]);
        //调用get方法,协程挂起,
        $cli->get('/index.php');
        //会等待i/o数据返回,执行wg的done方法,表示协程数据已返回
        $result[] = "6. get回baidu数据,唤起协程,此处被执行,正常这个先返回,因为ping百度更快,说明两个协程也是并发执行的, 第" . __LINE__ . "行, 执行时间" . timediff($time) . "<br/>";
        $cli->close();

        //放在协程的最后执行
        $wg->done();
    });
    //第二个协程get时挂起,执行到这一步
    $result[] = "5 cli->get时挂起协程了,此处被执行,不会被阻塞, 第" . __LINE__ . "行, 时间:" . microtime(true) . "<br/>";

    //堵塞中,直到所有的协程都执行调用done, 才会继续往下执行
    $wg->wait();
    $result[] = "总执行时间" . timediff($time) . ", 可看出约等于最长请求的时间而不是所有时间之和,协程间是真正并发执行的";
    $response->end(implode("<br/>", $result));
});
$http->start();

最终执行结果: 其实channel可以push任何数据,上面的示例中,我们用了一个result变量来存储结果,并且通过&$result的方式引用至协程内部了,如果我们把waitgroup代码稍改造一下,可以更优雅和方便一些:

<?php
class waitgroup
{

    private $count = 0;
    private $chan;

    /**
     * waitgroup constructor.
     * @desc 初始化一个channel
     */
    public function __construct()
    {
        $this->chan = new chan;
    }

    /**
     * @desc 计数+1
     * @调用时机:在起一个协程前
     */
    public function add()
    {
        $this->count++;
    }

    /**
     * @param $data
     * @desc 协程处理完成时调用,把数据存入channel
     */
    public function done($data)
    {

        $this->chan->push($data);
    }

    /**
     * @desc 堵塞的等待所有的协程处理完成并返回结果
     */
    public function wait()
    {
        $result = [];
        for ($i = 0; $i < $this->count; $i++) {
            //调用pop方法时,如果没有数据,此协程会挂起
            //当往chan中push数据后,协程会被恢复
            $result[] = $this->chan->pop();
        }

        return $result;
    }

}

我们给done方法加了一个$data参数,可以把处理结果push到channel中,那我们在wait成功后,可以取到结果

swoole的channel还是非常强大的,可以Push任何数据, 利用channel,让协程间的通讯变的非常简单可靠, 快快查看原文,了解更多swoole channel的使用姿势吧。

预告:下一篇将实现一个mysql连接池,深入理解channel和defer 功能

--------------伟大的分割线---------------- PHP饭米粒(phpfamily) 由一群靠谱的人建立,愿为PHPer带来一些值得细细品味的精神食粮! 饭米粒只发原创或授权发表的文章,不转载网上的文章 所发的文章,均可找到原作者进行沟通。 也希望各位多多打赏(算作稿费给文章作者),更希望大家多多投搞。 投稿请联系: shenzhe163@gmail.com

本文由 半桶水 授权 饭米粒 发布,转载请注明本来源信息和以下的二维码(长按可识别二维码关注)