版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。

swoole深入学习 8. 协程

swoole 在 2.0正式版加入了协程功能。这一章主要来深究一下在Swoole中如何使用协程。

什么是协程?

协程(Coroutine)也叫用户级线程, 很多人分不清楚协程和线程和进程的关系。进程(Process)是操作系统分配资源的单位,线程(Thread)是进程的一个实体,是CPU调度和分派的基本单位。线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。简单的说就是: 线程和进程的调度是由操作系统来调控, 而协程的调度由用户自己调控。 所以协程调度器可以在协程A即将进入阻塞IO操作, 比如 socket 的 read (其实已经设置为异步IO )之前, 将该协程挂起,把当前的栈信息 StackA 保存下来, 然后切换到协程B, 等到协程A的该 IO操作返回时, 再根据 StackA 切回到之前的协程A当时的状态。协程相对于事件驱动是一种更先进的高并发解决方案, 把复杂的逻辑和异步都封装在底层, 让程序员在编程时感觉不到异步的存在, 用响马的话就是【用同步抒写异步情怀】。

所以,你可以理解为协程就是用同步的方式来写异步回调的高并发程序。

值得注意的是:swoole协程与线程不同,在一个进程内创建的多个协程,实际上是串行的。同一CPU时间,只有一个协程在执行,因此swoole协程是阻塞运行的,语法也是用的同步的方式在写,只不过是在底层做了切换调度,提高的仅仅是单个进程接收请求的能力,并没有提高执行速度(总共需要的时间)

所以协程最大的功能就是提高了单个进程接受请求的能力,进而提高了总体高并发的能力。

swoole 支持的协程客户端

目前在swoole中支持的协程用的较多的有以下:

Swoole\Coroutine\Client
Swoole\Coroutine\Redis
Swoole\Coroutine\MySQL
Swoole\Coroutine\Http\Client
Swoole\Coroutine\PostgreSQL
Swoole\Coroutine\HTTP2\Client

我也会针对这些协成做一一讲解。

server中支持协程的回调方法列表

目前Swoole2仅有部分事件回调函数底层自动创建了协程,以下回调函数可以调用协程客户端 (文本用基于swoole 2.1.3版本):

1. swoole\server 下面的:

onWorkerStart
onClose
onConnect
onReceive
onPacket

2. swoole\websocket\server 下面的

onMessage
onHandShake
onOpen

3. swoole\http\server 下面的

onRequest

4. tick/after 定时器

及时的跟新请看官网:https://wiki.swoole.com/wiki/page/696.html

在新版本的中,在不支持协程的位置可以使用goCo::create创建协程。这些内容我会在下节中会单独讲。

Swoole\Coroutine\Client

Swoole\Coroutine\Client 提供了TCP和UDP传输协议Socket客户端的封装代码,使用时仅需new Swoole\Coroutine\Client即可。

直接看例子吧,我在swoole\http\serveronRequest里去调用tcp client协程:

<?php
$server = new Swoole\Http\Server("127.0.0.1", 9502, SWOOLE_BASE); $server->set([
'worker_num' => 1,
]); $server->on('Request', function ($request, $response) { //屏蔽Google浏览器发的favicon.ico请求
if ($request->server['path_info'] == '/favicon.ico' || $request->server['request_uri'] == '/favicon.ico') {
return $response->end();
} var_dump('stime:' . microtime(true)); $client = new Swoole\Coroutine\Client(SWOOLE_SOCK_TCP); var_dump('new:' . microtime(true)); //connect的三个参数: ip, 端口, 超时时间。
//超时时间单位是秒s,支持浮点数。默认为0.1s,即100ms,超时发生时,连接会被自动close掉。
if (!$client->connect('127.0.0.1', 9501, 0.5)) {
return $response->end(' swoole response error:' . $client->errCode);
} var_dump('connect:' . microtime(true)); // send 发送数据给server ,内容为字符串
$client->send("hello world\n"); var_dump('send:' . microtime(true)); // recv 接收数据 参数为超时时间,如果不设置以connect的为准。超时会自动close掉。
echo "from server: " $client->recv(5); var_dump('recv:' . microtime(true)); //close 关闭连接
$client->close(); $response->end('ok');
});
$server->start();
 

运行一下,然后在浏览器访问127.0.0.1:9502

string(21) "stime:1524051524.1339"
string(19) "new:1524051524.1343"
string(23) "connect:1524051524.1355"
string(20) "send:1524051524.1355"
from server: hello, 0
string(20) "recv:1524051528.1374"
 

通过打印时间,可以看出:conncet,send是没有阻塞的。会立即返回。recv是阻塞的,阻塞了4秒,这是因为我测试超时时间,在http server里返回的时候sleep了4秒。

conncet会切换唤起一次协程,但是是不阻塞的,会立即返回。 recv是 阻塞的,会唤起协程等待数据。send操作是立即返回的,没有协程切换。上面完全用同步的方式,来写异步阻塞回调,很流畅。

Swoole\Coroutine\Http\Client

这个是http的协程客户端,与swoole\http \client异步客户端的用法是一样的,都是异步的,只不过用到了协程切换机制,不需要写回调,直接用同步的方式来处理。

看一个例子,我在 tcp server onworkStart 回调了适用了协程:

  <?php

$serv = new Swoole\Server('0.0.0.0', 9503);

//初始化swoole服务
$serv->set(array(
'worker_num' => 1,
'daemonize' => false, //是否作为守护进程,此配置一般配合log_file使用
'max_request' => 1000,
'log_file' => './swoole.log',
// 'task_worker_num' => 8
)); //设置监听
$serv->on('Start', 'onStart');
$serv->on('Connect', 'onConnect');
$serv->on("Receive", 'onReceive');
$serv->on("Close", 'onClose'); $serv->on('WorkerStart', function ($serv, $workerId) { //创建http client协程 echo $workerId . PHP_EOL; //new
$cli = new Swoole\Coroutine\Http\Client('127.0.0.1', 9501); //设置请求头
$cli->setHeaders([
'Host' => "localhost",
"User-Agent" => 'Chrome/49.0.2587.3',
'Accept' => 'text/html,application/xhtml+xml,application/xml',
'Accept-Encoding' => 'gzip',
]); //设置超时时间
$cli->set(['timeout' => 5]); var_dump('connect:' . microtime(true)); //get方法,协程,会阻塞
$cli->get('/index.php'); var_dump('get:' . microtime(true)); echo $cli->body; var_dump('body:' . microtime(true)); $cli->close(); }); function onStart($serv)
{
//echo SWOOLE_VERSION . " onStart\n";
} function onConnect($serv, $fd)
{
echo $fd . "Client Connect.\n";
} function onReceive($serv, $fd, $from_id, $data)
{
echo "Get Message From Client {$fd}:{$data}\n";
// send a task to task worker. $serv->send($fd, "hello, " . $from_id); } function onClose($serv, $fd)
{
echo "Client Close.\n";
} //开启
$serv->start();
 

会输出:

0
string(23) "connect:1524110315.0407"
string(18) "get:1524110318.044"
hello!
string(20) "body:1524110318.0441"
 

我再http server里sleep了3秒,可以看出,get就用了3秒的时间,会阻塞住等待。

Swoole\Coroutine\Redis

redis 在平时用的非常多,基本在php中要么用phpredis的C扩展,要么用php语言版本的phpiredis。swoole里面也提供过异步的redis方案,但是由于需要层层回调,很是蛋疼。协程版本的redis就简单的多了。

需要安装一个第三方的异步Redis库hiredis,并且在编译swoole时增加--enable-coroutine--enable-async-redis来开启此功能。

直接上代码吧:

 $redis = new Swoole\Coroutine\Redis();
$res = $redis->connect('127.0.0.1', 6379);
$ret = $redis->set('coroutine_i', 50); //协程唤起,阻塞,但是写程序无感知
$redis->zAdd('key1', 1, 'val1');
$redis->zAdd('key1', 0, 'val0');
$redis->zAdd('key1', 5, 'val5');
var_dump($redis->zRange('key1', 0, -1, true)); $redis->close();
 

打印为:

array(6) {
[0]=>
string(4) "val0"
[1]=>
string(1) "0"
[2]=>
string(4) "val1"
[3]=>
string(1) "1"
[4]=>
string(4) "val5"
[5]=>
string(1) "5"
}
 

和 phpredis的调用方法几乎有一模一样,但是输出的格式会不一样,而且有如下坑:

1. $redis->get('no-exist-key'), get一个不存在的key 。返回的是 null ,不是 false。

2. $redis->zRevRange('key', 0, 19, true); 获取一个zset集合,结果集会不一样,不是键值对的。

3. redis 的连接,第三个参数是自动php序列化数据,要设置为false,或者不填,默认是false:redis->connect($host, $port, false)。设置为true, 会在zset数据读取出现问题。已知道的坑了。
 

尚未实现的Redis命令:

scan object sort migrate hscan sscan zscan
 

Swoole\Coroutine\MySQL

mysql协程,很简单,直接上代码

<?php
$server = new Swoole\Http\Server("127.0.0.1", 9502, SWOOLE_BASE); $server->set([
'worker_num' => 1,
]); $server->on('Request', function ($request, $response) { //屏蔽Google浏览器发的favicon.ico请求
if ($request->server['path_info'] == '/favicon.ico' || $request->server['request_uri'] == '/favicon.ico') {
return $response->end();
} var_dump('stime:' . microtime(true)); //new mysql
$db = new Swoole\Coroutine\MySQL(); var_dump('new:' . microtime(true)); $server = array(
'host' => '127.0.0.1',
'user' => 'root',
'password' => 'root',
'database' => 'rcs',
); //connect
$ret1 = $db->connect($server); var_dump('connect:' . microtime(true)); //直接query
$info = $db->query('SELECT 1+1;');
var_dump($info); //1. 先 prepare
$stmt = $db->prepare('SELECT id,risk_id FROM rcs_result WHERE id=? and risk_id=?'); var_dump('prepare:' . microtime(true)); if ($stmt == false) {
var_dump($db->errno, $db->error);
} else {
// 2. 配合 prepare,再execute
$ret2 = $stmt->execute(array(71, 60));
var_dump('execute:' . microtime(true)); var_dump($ret2); $ret3 = $stmt->execute(array(13, 12));
var_dump('execute:' . microtime(true)); var_dump($ret3);
} $response->end('ok'); });
$server->start();
 

比较简单,就不阐述了,但是可能会有坑,在线上慎用。

协程并发

协程其实也是阻塞运行的,如果,在一个执行中,比如同时查redis,再去查mysql,即使用了上面的协程,也是顺序执行的。那么可不可以几个协程并发执行呢?

答案当然是可以的,需要用延迟收包,当遇到IO 阻塞的时候,协程就挂起了,不会阻塞在那里等着网络回报,而是继续往下走。

swoole 协程调用里面可以用setDefer()方法声明延迟收包,然后通过recv()方法收包。

看下面这个例子:

<?php
$server = new Swoole\Http\Server("127.0.0.1", 9502, SWOOLE_BASE); $server->set([
'worker_num' => 1,
]); $server->on('Request', function ($request, $response) { //屏蔽Google浏览器发的favicon.ico请求
if ($request->server['path_info'] == '/favicon.ico' || $request->server['request_uri'] == '/favicon.ico') {
return $response->end();
} echo "#BEGIN :" . microtime(true) . PHP_EOL; // tcp
$tcpclient = new Swoole\Coroutine\Client(SWOOLE_SOCK_TCP);
$tcpclient->connect('127.0.0.1', 9501, 0.5);
$tcpclient->send("hello world\n"); echo "#after TCP:" . microtime(true) . PHP_EOL; //redis
$redis = new Swoole\Coroutine\Redis();
$redis->connect('127.0.0.1', 6379);
$redis->setDefer();
$redis->get('key'); echo "#after redis:" . microtime(true) . PHP_EOL; //mysql
$mysql = new Swoole\Coroutine\MySQL();
$mysql->connect([
'host' => '127.0.0.1',
'user' => 'root',
'password' => 'root',
'database' => 'rcs',
]);
$mysql->setDefer();
$b = $mysql->query('select sleep(10)');
var_dump("mysql, return:", $b); echo "#after MYSQL:" . microtime(true) . PHP_EOL; //http
$httpclient = new Swoole\Coroutine\Http\Client('0.0.0.0', 9599);
$httpclient->setHeaders(['Host' => "www.qq.com"]);
$httpclient->set(['timeout' => 1]);
$httpclient->setDefer();
$httpclient->get('/'); echo "#after HTTP:" . microtime(true) . PHP_EOL; //使用recv收报
$tcp_res = $tcpclient->recv();
echo "#recv tcp:" . microtime(true) . PHP_EOL; $redis_res = $redis->recv();
echo "#recv redis:" . microtime(true) . PHP_EOL; $mysql_res = $mysql->recv();
echo "#recv mysql:" . microtime(true) . PHP_EOL; $http_res = $httpclient->recv();
echo "#recv http:" . microtime(true) . PHP_EOL; echo "#finish :" . microtime(true) . PHP_EOL; $response->end('Test End');
});
$server->start();
 

我分别在各个点打印了时间点,用来看下执行的时间。打开浏览器

http://127.0.0.1:9502/
 

看下输出:

#BEGIN :1524136394.7842

#after TCP:1524136394.7877
#after redis:1524136394.7909
#after MYSQL:1524136394.799
#after HTTP:1524136394.7993 #recv tcp:1524136395.2986
#recv redis:1524136395.2986
#recv mysql:1524136404.7898 //阻塞了10秒
#recv http:1524136404.7898 #FINISH :1524136404.7898
 

前面的都是非阻塞调用,在收包的时候就是阻塞了。总共花了:10.0056秒。

那一摸一样的代码,我们不用延迟收报,看下时间:

<?php
$server = new Swoole\Http\Server("127.0.0.1", 9502, SWOOLE_BASE); $server->set([
'worker_num' => 1,
]); $server->on('Request', function ($request, $response) { //屏蔽Google浏览器发的favicon.ico请求
if ($request->server['path_info'] == '/favicon.ico' || $request->server['request_uri'] == '/favicon.ico') {
return $response->end();
} echo "#BEGIN :" . microtime(true) . PHP_EOL; $tcpclient = new Swoole\Coroutine\Client(SWOOLE_SOCK_TCP);
$tcpclient->connect('127.0.0.1', 9501, 0.5);
$tcpclient->send("hello world\n");
$tcpclient->recv(); //var_dump("tpc, return:", $tcpclient->recv()); echo "#after TCP:" . microtime(true) . PHP_EOL; $redis = new Swoole\Coroutine\Redis();
$redis->connect('127.0.0.1', 6379);
//$redis->setDefer();
$a = $redis->get('key');
var_dump("redis, return:", $a); echo "#after redis:" . microtime(true) . PHP_EOL; $mysql = new Swoole\Coroutine\MySQL();
$mysql->connect([
'host' => '127.0.0.1',
'user' => 'root',
'password' => 'root',
'database' => 'rcs',
]);
//$mysql->setDefer();
$b = $mysql->query('select sleep(10)');
var_dump("mysql, return:", $b); echo "#after MYSQL:" . microtime(true) . PHP_EOL; $httpclient = new Swoole\Coroutine\Http\Client('0.0.0.0', 9599);
$httpclient->setHeaders(['Host' => "www.qq.com"]);
$httpclient->set(['timeout' => 1]);
//$httpclient->setDefer();
$c = $httpclient->get('/');
var_dump("http, return:", $c); echo "#after HTTP:" . microtime(true) . PHP_EOL; echo "#FINISH :" . microtime(true) . PHP_EOL; $response->end('Test End');
});
$server->start();
 

打印如下:

#BEGIN :1524136719.7372

#after TCP:1524136720.2381

string(14) "redis, return:" NULL
#after redis:1524136720.2388 string(14) "mysql, return:"
array(1) {
[0]=>
array(1) {
["sleep(10)"]=>
string(1) "0"
}
}
#after MYSQL:1524136730.237 string(13) "http, return:" bool(false)
#after HTTP:1524136730.2375 #FINISH :1524136730.2375
 

花费时间为:10.5003秒。

好吧。比同步阻塞协程快了0.5秒。 多运行几次,发现快不了多少,因为是单个进程内的协程也是串行的。

总结

swoole 协程只是单纯的让异步代码用同步的方式来写,并没有提高一次进程内cgi 请求的执行速度,提高的是整个进程接受请求的能力,提高整体的qps。

swoole深入学习 8. 协程 转的更多相关文章

  1. Python学习---线程/协程/进程学习 1220【all】

    Python学习---线程基础学习 Python学习---线程锁/信号量/条件变量同步1221 Python学习---同步条件event/队列queue1223 Python学习---进程 1225 ...

  2. python学习笔记 协程

    在学习异步IO模型前,先来了解协程 协程又叫做微线程,Coroutine 子程序或者成为函数,在所有语言中都是层级调用,比如a调用b,b调用c.c执行完毕返回,b执行完毕返回,最后a执行完毕返回 所以 ...

  3. Swoole 同步模式与协程模式的对比

    在现代化 PHP 高级开发中,Swoole 为 PHP 带来了更多可能,如:常驻内存.协程,关于传统的 Apache/FPM 模式与常驻内存模式(同步)的巨大差异,之前我做过测试,大家能直观的感受到性 ...

  4. python学习之-- 协程

    协程(coroutine)也叫:微线程,是一种用户态的轻量级线程,就是在单线程下实现并发的效果.优点:1:无需线程上下文切换的开销.(就是函数之间来回切换)2:无需原子操作锁定及同步的开销.(如改一个 ...

  5. Python学习之协程

    8.8 协程 ​ 我们都知道线程间的任务切换是由操作系统来控制的,而协程的出现,就是为了减少操作系统的开销,由协程来自己控制任务的切换 ​ 协程本质上就是线程.既然能够切换任务,所以线程有两个最基本的 ...

  6. Python学习笔记--协程asyncio

    协程的主要功能是单线程并发运行 假设有3个耗时不一样的任务.看看协程的效果. 先来看没有使用协程情况: #!/usr/bin/python3 # -*- coding:utf-8 -*- import ...

  7. Swoole 协程与 Go 协程的区别

    Swoole 协程与 Go 协程的区别 进程.线程.协程的概念 进程是什么? 进程就是应用程序的启动实例. 例如:打开一个软件,就是开启了一个进程. 进程拥有代码和打开的文件资源,数据资源,独立的内存 ...

  8. swoole 协程介绍

    协程的执行顺序: 1 2 3 4 5 6 7 8 9 go(function () {     echo "hello go1 \n"; });   echo "hell ...

  9. Swoole 协程简介

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

随机推荐

  1. CNCF基金会的Certified Kubernetes Administrator认证考试计划

    关于CKA考试 CKA(Certified Kubernetes Administrator)是CNCF基金会(Cloud Native Computing Foundation)官方推出的Kuber ...

  2. 安卓模拟器Android SDK 4.0.3 R2安装完整图文教程

    在最新的Android 4.0.3 R2模拟器中,已经加入了GPU支持,可以支持OpenGL ES 2.0标准,让开发者可以借助模拟器来测试自己的OpenGL游戏.在去年新增了摄像头支持之后,现在的新 ...

  3. [CareerCup] 2. Bomberman 炸弹人

    We have a 2D grid. Each cell is either a wall, an enemy or empty. For example (0-empty, X-enemy, Y-w ...

  4. mysql:服务无法启动解决

    输入mysqld --console,查看报错 Found option without preceding group in config file E:\mysql-5.7.21-winx64\m ...

  5. page工具类

    工具类 /** * @Title: PageUtil.java * @Package * @Description: TODO(用一句话描述该文件做什么) * @author licy * @date ...

  6. 彻底理解js中this的指向,不必硬背(转)

    转自: http://www.h5cn.com/js/jishu/2016/0226/18248.html 首先必须要说的是,this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定th ...

  7. springboot使用activemq同时接收queue和topic消息

    原文链接:https://blog.csdn.net/jia_costa/article/details/79354478 新建springboot项目, pom文件如下 <?xml versi ...

  8. mysql常用操作及常见问题

    常用操作 mysql备份: --整库备份 docker exec 容器ID mysqldump -uroot -p密码 --databases 库名 > 库名.sql --仅导出表和数据 mys ...

  9. 【转帖】计算机网络协议(三)——UDP、TCP、Socket

    计算机网络协议(三)——UDP.TCP.Socket 2019年09月04日 11:09:41 to_be_better_one 阅读数 28794 文章标签: 计算机网络UDPTCPSocket 更 ...

  10. Jmeter plugins 之 Perfmon Metrics Collector(服务器性能监控)

    客户端(Jmeter端) 1.安装plugins manager,然后安装  2.添加listener-(第1步成功后才可看到此功能)  服务端:(要监控的服务器) 1.下载ServerAgent,并 ...