【swoole.2.06】协程并发执行耗时操作,延迟收包,协程开发需要注意的事项

前言

有关协程基本的知识前面已经说过了,协程的中心思想其实就是在IO阻塞的时候保存栈信息并让出代码执行权,等IO操作有返回后根据保存的栈信息切换回协程完成操作。这里就来模拟一下开发中可能会遇到的协程并发调用第三方接口及我自己和朋友们曾经碰到过的问题。

协程并发执行耗时操作

情景

现在有一个功能需要依托在有节假日信息的数据上,那么现在第一步就是爬节假日数据了。这种事情一般都会使用第三方接口,但是第三方接口网络请求会有一定的io阻塞导致变慢,那么完全就可以将这部分io阻塞挂起使用协程处理

说明

获取节假日接口:http://timor.tech/api/holiday/info/2019-10-01

返回数据格式:{"code":0,"type":{"type":2,"name":"国庆节","week":2},"holiday":{"holiday":true,"name":"国庆节","wage":3,"date":"2019-10-01"}}

返回数据格式2:{"code":0,"type":{"type":0,"name":"周三","week":3},"holiday":null}

非协程代码测试

$count = 0;
$start = strtotime("2019-01-01");
$baseUrl = 'http://timor.tech/api/holiday/info/';
while (true) {
    $day = date('Y-m-d', strtotime("+{$count} day", $start));
    $count++;
    $res = json_decode(file_get_contents($baseUrl . $day), 1);
    if (!empty($res['holiday'])) {
        echo $res['holiday']['date'] . " : " . $res['holiday']['name'] . PHP_EOL;
    }
    if (date('Y', strtotime("+{$count} day", $start)) == date('Y', strtotime('+1 year'))) {
        break;
    }
}
[root@iZbp1acp86oa3ixxw4n1dpZ 2.06]# time php holiday.php 
2019-01-01 : 元旦
2019-02-02 : 春节前调休
......完整输出请看最下面的附录
2019-10-07 : 国庆节
2019-10-12 : 国庆节后调休

real	0m31.010s
user	0m0.076s
sys	0m0.103s

耗时31秒

协程化尝试

$count = 0;
$start = strtotime("2019-01-01");
while (true) {
    if (date('Y', strtotime("+{$count} day", $start)) == date('Y', strtotime('+1 year'))) {
        break;
    }
    $count++;
    $day = date('Y-m-d', strtotime("+{$count} day", $start));
    go(function () use ($day) {
        $baseUrl = 'timor.tech';
        $uri = "/api/holiday/info/" . $day;
        $cli = new \Swoole\Coroutine\Http\Client($baseUrl, 80);
        $cli->get($uri);
        $res = json_decode($cli->body, 1);
        if (!empty($res['holiday'])) {
            echo $res['holiday']['date'] . " : " . $res['holiday']['name'] . PHP_EOL;
        }
    });
}
[root@iZbp1acp86oa3ixxw4n1dpZ 2.06]# time php holiday.php 
2019-02-10 : 春节
2019-02-08 : 春节
......完整输出请看最下面的附录
2019-10-12 : 国庆节后调休
2020-01-01 : 元旦

real	0m1.378s
user	0m0.062s
sys	0m0.074s

耗时1.378秒

结论

可以很清楚的看到,使用协程化处理需要耗时io操作的第三方接口,大大节省了请求时间。

注意事项

需要注意的是,不是所有代码都可以在协程中让出执行权限的。php中同步阻塞函数如下

  1. mysql、mysqli、pdo以及其他DB操作函数
  2. sleep、usleep
  3. curl
  4. stream、socket扩展的函数
  5. swoole_client同步模式
  6. memcache、redis扩展函数
  7. file_get_contents/fread等文件读取函数
  8. swoole_server->taskwait
  9. swoole_server->sendwait

这也是为什么在上面的协程代码中没有使用最简单的file_get_contents,因为使用后虽然在协程环境中,但是该协程在遇到file_get_contents时并不会自动让出执行权限,而是依旧阻塞的,对性能没有任何提升。不过好在swoole已经对上述情况给出了解决的类:

  1. Coroutine\Client
  2. Coroutine\Server
  3. Coroutine\Http\Client
  4. Coroutine\Http2\Client
  5. Coroutine\Redis
  6. Coroutine\Socket
  7. Coroutine\System
  8. Coroutine\MySQL
  9. Coroutine\PostgreSQL
  10. 4.4版本以上还提供了Coroutine\Http\Server,完全协程化的Http服务器实现。

开发的时候请注意协程中是否使用可能会导致阻塞的函数,使用后会导致阻塞后无法让出执行权限导致性能无法提高。

延迟收包

上面说到了在协程中调用第三方接口,在网络IO阻塞的时候让出执行权限提高执行效率的案例。但是大部分情况下在调用玩第三方接口后需要等待返回值并处理,比如上面的案例,如果需要记录所有节假日并用于后续操作怎么办呢?协程外怎么使用协程内的数据呢?这里有两种方法,通道+子协程延迟收包

通道+子协程

关于通道,后面可能会单独拿出一篇来细讲,先简单说一下好了。

通道,类似于go语言的chan,支持多生产者协程和多消费者协程。底层自动实现了协程的切换和调度。

通道与PHP的Array类似,仅占用内存,没有其他额外的资源申请,所有操作均为内存操作,无IO消耗 底层使用PHP引用计数实现,无内存拷贝。即使是传递巨大字符串或数组也不会产生额外性能消耗

Channel->push :当队列中有其他协程正在等待pop数据时,自动按顺序唤醒一个消费者协程。当队列已满时自动yield让出控制器,等待其他协程消费数据 Channel->pop:当队列为空时自动yield,等待其他协程生产数据。消费数据后,队列可写入新的数据,自动按顺序唤醒一个生产者协程。 Coroutine\Channel使用本地内存,不同的进程之间内存是隔离的。只能在同一进程的不同协程内进行push和pop操作

需要注意的是,使用Channel->pop是阻塞的,在通道内有数据前不会执行下面的代码,所以也不用担心在使用Channel->pop的时候通道内没有数据,因为程序会阻塞在这里等待其他程序向通道内写入数据。另外也正是这个原因,请千万注意不要再通道内没数据并未来也不会写数据的时候使用pop,否则会造成完全阻塞

通道+子协程常识

go(function () {
    $count = 0;
    $start = strtotime("2019-01-01");
    $channel = new \Swoole\Coroutine\Channel(1024);
    echo "开始创建请求" . PHP_EOL;
    while (true) {
        if (date('Y', strtotime("+{$count} day", $start)) == date('Y', strtotime('+1 year'))) {
            break;
        }
        $count++;
        $day = date('Y-m-d', strtotime("+{$count} day", $start));
        go(function () use ($day, $channel) {
            $baseUrl = 'timor.tech';
            $uri = "/api/holiday/info/" . $day;
            $cli = new \Swoole\Coroutine\Http\Client($baseUrl, 80);
            $cli->get($uri);
            $channel->push($cli->body);
        });
    }
    echo "开始使用通道收包" . PHP_EOL;
    for ($i = 0; $i < $count; $i++) {
        $res = $channel->pop();
        $res = json_decode($res, 1);
        if (!empty($res['holiday'])) {
            echo $res['holiday']['date'] . " : " . $res['holiday']['name'] . PHP_EOL;
        }
    }
});
[root@iZbp1acp86oa3ixxw4n1dpZ 2.06]# time php holiday.php 
开始创建请求
开始使用通道收包
2019-02-03 : 春节前调休
2019-02-09 : 春节
......完整输出请看最下面的附录
2019-10-05 : 国庆节
2020-01-01 : 元旦

real	0m1.289s
user	0m0.060s
sys	0m0.078s
[root@iZbp1acp86oa3ixxw4n1dpZ 2.06]# 

也仅仅使用了1.289秒,同时可以看到,接受数据是在创建完所有并发请求之后的

延迟收包

绝大部分协程组件,都支持了setDefer特性。可以将请求响应式的接口拆分为两个步骤,使用此机制可以实现先发送数据, 再并发收取响应结果。

由于大多数情况下, [建立连接和发送数据的耗时] 相比于 [等待响应的耗时] 来说可以忽略不计, 所以可以简单理解为defer模式下, 多个客户端的请求响应是并发的。

使用setDefer()方法声明需要延迟收包,然后通过recv()方法来收包。

延迟收包尝试

go(function () {
    $count = 0;
    $start = strtotime("2019-01-01");
    $client = [];
    while (true) {
        if (date('Y', strtotime("+{$count} day", $start)) == date('Y', strtotime('+1 year'))) {
            break;
        }
        $count++;
        $day = date('Y-m-d', strtotime("+{$count} day", $start));
        $baseUrl = 'timor.tech';
        $uri = "/api/holiday/info/" . $day;
        $cli = new \Swoole\Coroutine\Http\Client($baseUrl, 80);
        $cli->setDefer(true);
        $cli->get($uri);
        $client[] = $cli;
    }
    foreach ($client as $cli) {
        $cli->recv();
        $res = json_decode($cli->body, 1);
        if (!empty($res['holiday'])) {
            echo $res['holiday']['date'] . " : " . $res['holiday']['name'] . PHP_EOL;
        }
    }
});
[root@iZbp1acp86oa3ixxw4n1dpZ 2.06]# time php holiday.php 
2019-02-02 : 春节前调休
2019-02-03 : 春节前调休
......完整输出请看最下面的附录
2019-10-12 : 国庆节后调休
2020-01-01 : 元旦

real	0m15.008s
user	0m0.069s
sys	0m0.055s

这次使用了15秒,并不是想象中的1秒。究其原因,文档如是说道:

由于大多数情况下, [建立连接和发送数据的耗时] 相比于 [等待响应的耗时] 来说可以忽略不计, 所以可以简单理解为defer模式下, 多个客户端的请求响应是并发的。 需注意的是, defer特性只支持并发收取响应结果, 正如示例代码所示, 创建连接和数据的发送, 仍是串行的

使用协程http客户端的延迟收包setDefer特性并不是完全异步处理请求,客户端的协程化挂起时间是在建立连接和发送数据后,虽然是并发请求接口,但是仅节省了等待响应的耗时,在创建连接和数据的发送, 仍是串行的。

单独测试一下四种方式的时间成本:

  1. 阻塞请求一次接口
  2. 创建协程环境并在协程中单独请求接口(此时时间成本应为创建协程的时间)
  3. 创建协程环境并在协程中创建子协程请求接口后将数据放入通道(此时时间成本应为创建协程的时间,与上面相同)
  4. 在协程环境中使用协程的Http/Client客户端的setDefer特性并发请求(此时时间成本应从创建对象开始到$cli->get()结束)
阻塞请求接口
for ($i = 0; $i < 5; $i++) {
    $time = microtime(true);
    file_get_contents('http://timor.tech/api/holiday/info/2019-10-01');
    echo microtime(true) - $time . PHP_EOL;
}
[root@iZbp1acp86oa3ixxw4n1dpZ 2.06]# time php holiday.php 
0.080605030059814
0.084717035293579
0.089498043060303
0.088149070739746
0.084078073501587

平均耗时0.085秒

创建协程环境并在协程中单独请求接口
for ($i = 0; $i < 5; $i++) {
    $time = microtime(true);
    go(function () {
        $baseUrl = 'timor.tech';
        $uri = "/api/holiday/info/2019-10-01";
        $cli = new \Swoole\Coroutine\Http\Client($baseUrl, 80);
        $cli->get($uri);
    });
    echo microtime(true) - $time . PHP_EOL;
}
[root@iZbp1acp86oa3ixxw4n1dpZ 2.06]# time php holiday.php 
0.00043106079101562
2.8133392333984E-5
2.3126602172852E-5
1.4066696166992E-5
0.00017905235290527

时间已经在毫秒级了

在协程环境中使用协程的Http/Client客户端的setDefer特性并发请求
go(function () {
    for ($i = 0; $i < 5; $i++) {
        $baseUrl = 'timor.tech';
        $uri = "/api/holiday/info/2019-10-01";
        $cli = new \Swoole\Coroutine\Http\Client($baseUrl, 80);
        $time = microtime(true);
        $cli->setDefer(true);
        $cli->get($uri);
        echo microtime(true) - $time . PHP_EOL;
    }
});
[root@iZbp1acp86oa3ixxw4n1dpZ 2.06]# time php holiday.php 
0.041023015975952
0.044267892837524
0.035816192626953
0.043223142623901
0.042073965072632

时间成本在0.04秒左右

结束了

那么今天的使用swoole并发处理耗时请求,你学到了吗?这里仅演示了发送http请求第三方接口的场景,使用协程mysql的场景会有一些问题,这里模拟一下,在下一篇会针对这个情况做特殊的处理。

请注意:在没弄懂协程化mysql之前不要轻易尝试并发mysql,redis等处理数据,会造成的结果如下,这里演示mysql

go(function () {
    $mysqlConfig = require_once __DIR__ . '/mysql.config.php';
    for ($i = 0; $i < 20; $i++) {
        go(function () use ($mysqlConfig) {
            $swoole_mysql = new Swoole\Coroutine\MySQL();
            $swoole_mysql->connect($mysqlConfig);
            $swoole_mysql->query('select sleep(10)');
        });
    }
});
mysql> show processlist;
+-------+------+-----------------+------+---------+------+------------+------------------+
| Id    | User | Host            | db   | Command | Time | State      | Info             |
+-------+------+-----------------+------+---------+------+------------+------------------+
| 49477 | root | localhost:60512 | NULL | Query   |    0 | init       | show processlist |
| 49478 | root | localhost:60516 | test | Query   |    2 | User sleep | select sleep(10) |
| 49479 | root | localhost:60518 | test | Query   |    2 | User sleep | select sleep(10) |
| 49480 | root | localhost:60520 | test | Query   |    2 | User sleep | select sleep(10) |
| 49481 | root | localhost:60522 | test | Query   |    2 | User sleep | select sleep(10) |
| 49482 | root | localhost:60524 | test | Query   |    2 | User sleep | select sleep(10) |
| 49483 | root | localhost:60526 | test | Query   |    2 | User sleep | select sleep(10) |
| 49484 | root | localhost:60528 | test | Query   |    2 | User sleep | select sleep(10) |
| 49485 | root | localhost:60530 | test | Query   |    2 | User sleep | select sleep(10) |
| 49486 | root | localhost:60532 | test | Query   |    2 | User sleep | select sleep(10) |
| 49487 | root | localhost:60534 | test | Query   |    2 | User sleep | select sleep(10) |
| 49488 | root | localhost:60536 | test | Query   |    2 | User sleep | select sleep(10) |
| 49489 | root | localhost:60538 | test | Query   |    2 | User sleep | select sleep(10) |
| 49490 | root | localhost:60540 | test | Query   |    2 | User sleep | select sleep(10) |
| 49491 | root | localhost:60542 | test | Query   |    2 | User sleep | select sleep(10) |
| 49492 | root | localhost:60544 | test | Query   |    2 | User sleep | select sleep(10) |
| 49493 | root | localhost:60546 | test | Query   |    2 | User sleep | select sleep(10) |
| 49494 | root | localhost:60548 | test | Query   |    2 | User sleep | select sleep(10) |
| 49495 | root | localhost:60550 | test | Query   |    2 | User sleep | select sleep(10) |
| 49496 | root | localhost:60552 | test | Query   |    2 | User sleep | select sleep(10) |
| 49497 | root | localhost:60554 | test | Query   |    2 | User sleep | select sleep(10) |
+-------+------+-----------------+------+---------+------+------------+------------------+
21 rows in set (0.00 sec)

因为每个协程mysql都起了一个mysql连接,所以在这里可以看到,确实减少了php的IO等待,但是慢sql会导致mysql连接数激增,如果在高峰期大量使用并发mysql操作可能会导致mysql相关的问题!

附录

原始和协程化请求节假日信息的完整输出

原始
[root@iZbp1acp86oa3ixxw4n1dpZ 2.06]# time php holiday.php 
2019-01-01 : 元旦
2019-02-02 : 春节前调休
2019-02-03 : 春节前调休
2019-02-04 : 除夕
2019-02-05 : 春节
2019-02-06 : 春节
2019-02-07 : 春节
2019-02-08 : 春节
2019-02-09 : 春节
2019-02-10 : 春节
2019-04-05 : 清明节
2019-04-06 : 清明节
2019-04-07 : 清明节
2019-04-28 : 劳动节前调休
2019-05-01 : 劳动节
2019-05-02 : 劳动节
2019-05-03 : 劳动节
2019-05-04 : 劳动节
2019-05-05 : 劳动节后调休
2019-06-07 : 端午节
2019-06-08 : 端午节
2019-06-09 : 端午节
2019-09-13 : 中秋节
2019-09-14 : 中秋节
2019-09-15 : 中秋节
2019-09-29 : 国庆节前调休
2019-10-01 : 国庆节
2019-10-02 : 国庆节
2019-10-03 : 国庆节
2019-10-04 : 国庆节
2019-10-05 : 国庆节
2019-10-06 : 国庆节
2019-10-07 : 国庆节
2019-10-12 : 国庆节后调休

real	0m31.010s
user	0m0.076s
sys	0m0.103s
协程化处理
[root@iZbp1acp86oa3ixxw4n1dpZ 2.06]# time php holiday.php 
2019-02-10 : 春节
2019-02-08 : 春节
2019-02-03 : 春节前调休
2019-02-09 : 春节
2019-04-06 : 清明节
2019-02-02 : 春节前调休
2019-04-07 : 清明节
2019-02-07 : 春节
2019-04-28 : 劳动节前调休
2019-02-06 : 春节
2019-05-02 : 劳动节
2019-05-05 : 劳动节后调休
2019-02-05 : 春节
2019-02-04 : 除夕
2019-04-05 : 清明节
2019-05-01 : 劳动节
2019-05-03 : 劳动节
2019-06-09 : 端午节
2019-05-04 : 劳动节
2019-06-08 : 端午节
2019-06-07 : 端午节
2019-10-02 : 国庆节
2019-09-15 : 中秋节
2019-10-07 : 国庆节
2019-10-01 : 国庆节
2019-10-03 : 国庆节
2019-09-13 : 中秋节
2019-10-06 : 国庆节
2019-10-04 : 国庆节
2019-10-05 : 国庆节
2019-09-14 : 中秋节
2019-09-29 : 国庆节前调休
2019-10-12 : 国庆节后调休
2020-01-01 : 元旦

real	0m1.378s
user	0m0.062s
sys	0m0.074s
子协程+通道实现延迟收包
[root@iZbp1acp86oa3ixxw4n1dpZ 2.06]# time php holiday.php 
开始创建请求
开始使用通道收包
2019-02-03 : 春节前调休
2019-02-09 : 春节
2019-02-10 : 春节
2019-02-07 : 春节
2019-02-05 : 春节
2019-02-02 : 春节前调休
2019-02-04 : 除夕
2019-02-06 : 春节
2019-05-03 : 劳动节
2019-04-28 : 劳动节前调休
2019-05-02 : 劳动节
2019-02-08 : 春节
2019-05-04 : 劳动节
2019-06-07 : 端午节
2019-04-05 : 清明节
2019-06-08 : 端午节
2019-04-06 : 清明节
2019-05-01 : 劳动节
2019-05-05 : 劳动节后调休
2019-04-07 : 清明节
2019-06-09 : 端午节
2019-10-07 : 国庆节
2019-10-06 : 国庆节
2019-10-12 : 国庆节后调休
2019-09-15 : 中秋节
2019-09-14 : 中秋节
2019-10-01 : 国庆节
2019-10-04 : 国庆节
2019-10-03 : 国庆节
2019-10-02 : 国庆节
2019-09-13 : 中秋节
2019-09-29 : 国庆节前调休
2019-10-05 : 国庆节
2020-01-01 : 元旦

real	0m1.289s
user	0m0.060s
sys	0m0.078s
[root@iZbp1acp86oa3ixxw4n1dpZ 2.06]# 
延迟收包处理结果
[root@iZbp1acp86oa3ixxw4n1dpZ 2.06]# time php holiday.php 
2019-02-02 : 春节前调休
2019-02-03 : 春节前调休
2019-02-04 : 除夕
2019-02-05 : 春节
2019-02-06 : 春节
2019-02-07 : 春节
2019-02-08 : 春节
2019-02-09 : 春节
2019-02-10 : 春节
2019-04-05 : 清明节
2019-04-06 : 清明节
2019-04-07 : 清明节
2019-04-28 : 劳动节前调休
2019-05-01 : 劳动节
2019-05-02 : 劳动节
2019-05-03 : 劳动节
2019-05-04 : 劳动节
2019-05-05 : 劳动节后调休
2019-06-07 : 端午节
2019-06-08 : 端午节
2019-06-09 : 端午节
2019-09-13 : 中秋节
2019-09-14 : 中秋节
2019-09-15 : 中秋节
2019-09-29 : 国庆节前调休
2019-10-01 : 国庆节
2019-10-02 : 国庆节
2019-10-03 : 国庆节
2019-10-04 : 国庆节
2019-10-05 : 国庆节
2019-10-06 : 国庆节
2019-10-07 : 国庆节
2019-10-12 : 国庆节后调休
2020-01-01 : 元旦

real	0m15.008s
user	0m0.069s
sys	0m0.055s

程序幼儿员-龚学鹏
请先登录后发表评论
  • latest comments
  • 总共0条评论