swoole深入学习 8. 协程 转
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
在新版本的中,在不支持协程的位置可以使用go
或Co::create
创建协程。这些内容我会在下节中会单独讲。
Swoole\Coroutine\Client
Swoole\Coroutine\Client 提供了TCP和UDP传输协议Socket客户端的封装代码,使用时仅需new Swoole\Coroutine\Client即可。
直接看例子吧,我在swoole\http\server
的onRequest
里去调用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. 协程 转的更多相关文章
- Python学习---线程/协程/进程学习 1220【all】
Python学习---线程基础学习 Python学习---线程锁/信号量/条件变量同步1221 Python学习---同步条件event/队列queue1223 Python学习---进程 1225 ...
- python学习笔记 协程
在学习异步IO模型前,先来了解协程 协程又叫做微线程,Coroutine 子程序或者成为函数,在所有语言中都是层级调用,比如a调用b,b调用c.c执行完毕返回,b执行完毕返回,最后a执行完毕返回 所以 ...
- Swoole 同步模式与协程模式的对比
在现代化 PHP 高级开发中,Swoole 为 PHP 带来了更多可能,如:常驻内存.协程,关于传统的 Apache/FPM 模式与常驻内存模式(同步)的巨大差异,之前我做过测试,大家能直观的感受到性 ...
- python学习之-- 协程
协程(coroutine)也叫:微线程,是一种用户态的轻量级线程,就是在单线程下实现并发的效果.优点:1:无需线程上下文切换的开销.(就是函数之间来回切换)2:无需原子操作锁定及同步的开销.(如改一个 ...
- Python学习之协程
8.8 协程 我们都知道线程间的任务切换是由操作系统来控制的,而协程的出现,就是为了减少操作系统的开销,由协程来自己控制任务的切换 协程本质上就是线程.既然能够切换任务,所以线程有两个最基本的 ...
- Python学习笔记--协程asyncio
协程的主要功能是单线程并发运行 假设有3个耗时不一样的任务.看看协程的效果. 先来看没有使用协程情况: #!/usr/bin/python3 # -*- coding:utf-8 -*- import ...
- Swoole 协程与 Go 协程的区别
Swoole 协程与 Go 协程的区别 进程.线程.协程的概念 进程是什么? 进程就是应用程序的启动实例. 例如:打开一个软件,就是开启了一个进程. 进程拥有代码和打开的文件资源,数据资源,独立的内存 ...
- swoole 协程介绍
协程的执行顺序: 1 2 3 4 5 6 7 8 9 go(function () { echo "hello go1 \n"; }); echo "hell ...
- Swoole 协程简介
什么是协程 协程可以简单理解为线程,只不过这个线程是用户态的,不需要操作系统参与,创建.销毁和切换的成本都非常低. 协程不能利用多核 cpu,想利用多核 cpu 需要依赖 Swoole 的多进程模型. ...
随机推荐
- Mysql迁移由于字符集导致乱码的数据
有时候会在不注意的情况下创建了字符集为latin1的数据库,导致后续插入的中文显示乱码.这时有两种方法:1.修改数据库与数据表的字符集(只能向上调整,不能向下调整):2.数据迁移.但是两种方法都需要做 ...
- LeetCode_119. Pascal's Triangle II
119. Pascal's Triangle II Easy Given a non-negative index k where k ≤ 33, return the kth index row o ...
- iframe重定向让父页面跳转
情景描述 我们在使用一些后台程序的html模板(比如H-ui)的时候,这些html前端程序是iframe版的, 也就是说在使用的时候,每当我点击左侧导航栏的一个按钮,在右侧就会弹出一个菜单栏,在显示的 ...
- react中如何处理日期格式整理
1.第一种模式——对应组件:DatePicker: 需要引入 import moment from "moment"; values.cfjdrq = moment(values. ...
- mysql修改时区的几种方法
说明: 以下记录修改mysql时区的几种方法. 具体:方法一:通过mysql命令行模式下动态修改1.1 查看mysql当前时间,当前时区 > select curtime(); #或select ...
- 100/200/400GE高速以太网:Autoneg & Link Training 自适应及链路学习相关姿势介绍
2019-10-31 08:29:22 先写个目录,陆续补齐 PAM4模式下50GE,100GE,200GE,400GE以太网为什么需要AN & LT功能?AN .Autoneg自适应功能介绍 ...
- darknet标签转化为COCO标签
import sys import json import cv2 import os import shutil dataset = { "info": { "desc ...
- (CVE-2015-0240)Samba远程代码执行
简介 Samba 是利用 SMB 协议实现文件共享的一款著名开源工具套件.日前 Samba 曝出一个严重安全漏洞,该漏洞出现在 smbd 文件服务端,漏洞编号为 CVE-2015-0240,可以允许攻 ...
- 使用Apache服务部署网站(基于IP,域名,端口)
本篇主要学习Apache网站服务程序的基本部署,基于IP地址.主机名(域名).端口号的虚拟主机功能. 1.基于IP地址 首先我们需要在虚拟机中线安装Apache服务程序,Apache服务程序的软件包名 ...
- centos7配置hadoop
hadoop压缩包下载: 链接:https://pan.baidu.com/s/1dz0Hh75VNKEebcYcbN-4Hw 提取码:g2e3 java压缩包下载: 链接:https://pan.b ...