有关协程基本的知识前面已经说过了,协程的中心思想其实就是在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中同步阻塞函数如下
这也是为什么在上面的协程代码中没有使用最简单的file_get_contents
,因为使用后虽然在协程环境中,但是该协程在遇到file_get_contents
时并不会自动让出执行权限,而是依旧阻塞的,对性能没有任何提升。不过好在swoole已经对上述情况给出了解决的类:
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
特性并不是完全异步处理请求,客户端的协程化挂起时间是在建立连接和发送数据
后,虽然是并发请求接口,但是仅节省了等待响应的耗时
,在创建连接和数据的发送, 仍是串行的。
单独测试一下四种方式的时间成本:
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
时间已经在毫秒级了
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
本文为龚学鹏原创文章,转载无需和我联系,但请注明来自龚学鹏博客http://www.noobcoder.cn
最新评论