Swoole在2.0开始内置协程(Coroutine)的能力,提供了具备协程能力IO接口(统一在名空间Swoole\Coroutine\*
)。
2.0.2或更高版本已支持PHP7
协程可以理解为纯用户态的线程,其通过协作而不是抢占来进行切换。相对于进程或者线程,协程所有的操作都可以在用户态完成,创建和切换的消耗更低。Swoole可以为每一个请求创建对应的协程,根据IO的状态来合理的调度协程,这会带来了以下优势:
开发者可以无感知的用同步的代码编写方式达到异步IO的效果和性能,避免了传统异步回调所带来的离散的代码逻辑和陷入多层回调中导致代码无法维护。
同时由于swoole是在底层封装了协程,所以对比传统的php层协程框架,开发者不需要使用yield关键词来标识一个协程IO操作,所以不再需要对yield的语义进行深入理解以及对每一级的调用都修改为yield,这极大的提高了开发效率。
协程API目前针对了TCP,UDP等主流协议client的封装,包括:
可以满足大部分开发者的需求。对于私有协议,开发者可以使用协程的TCP或者UDP接口去方便的封装。
swoole_server
或者swoole_http_server
进行开发,目前只支持在onRequet
, onReceive
, onConnect
事件回调函数中使用协程。swoole2.0需要通过添加--enable-coroutine
编译参数启用协程能力,示例如下:
phpize
./configure --with-php-config={path-to-php-config} --enable-coroutine
make
make install
添加编译参数,swoole server将切换到协程模式。
开启协程模式后,swoole_server
和swoole_http_server
将以为每一个请求创建对应的协程,开发者可以在onRequet
、onReceive
、onConnect
3个事件回调中使用协程客户端。
在Swoole\Server
的set方法中增加了一个配置参数max_coro_num
,用于配置一个worker进程最多同时处理的协程数目。因为随着worker进程处理的协程数目的增加,其占用的内存也会增加,为了避免超出php的memory_limit
限制,请根据实际业务的压测结果设置该值,默认为3000。
当代码执行到connect()和recv()
函数时,swoole会触发进行协程切换,此时swoole可以去处理其他的事件或者接受新的请求。当此client连接
成功或者后端服务回包
后,swoole server会恢复协程上下文,代码逻辑继续从切换点开始恢复执行。开发者整个过程不需要关心整个切换过程。具体使用可以参考client的文档。
__call()
dereferencing pointer ‘v.327’ does break strict-aliasing rules
、dereferencing type-punned pointer will break strict-aliasing rules
请手动编辑Makefile,将CFLAGS = -Wall -pthread -g -O2
替换为CFLAGS = -Wall -pthread -g -O2 -fno-strict-aliasing
,然后重新编译make clean;make;make install
bool getDefer();
bool setDefer([bool $is_defer = true]);
mixed recv();
在协程版本的Client中,实现了多个客户端并发的发包功能。
通常,如果一个业务请求中需要做一次redis请求和一次mysql请求,那么网络IO会是这样子:
redis发包->redis收包->mysql发包->mysql收包
以上流程网络IO的时间就等于 redis网络IO时间 + mysql网络IO时间。
而对于协程版本的Client,网络IO可以是这样子:
redis发包->mysql发包->redis收包->mysql收包
以上流程网络IO的时间就接近于 MAX(redis网络IO时间, mysql网络IO时间)。
现在支持并发请求的Client有:
除了Swoole\Coroutine\Client,其他Client都实现了defer特性,用于声明延迟收包。
因为Swoole\Coroutine\Client的发包和收包方法是分开的,所以就不需要实现defer特性了,而其他Client的发包和收包都是在一个方法中,所以需要一个setDefer()方法声明延迟收包,然后通过recv()方法收包。
协程版本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) {
$tcpclient = new Swoole\Coroutine\Client(SWOOLE_SOCK_TCP);
$tcpclient->connect('127.0.0.1', 9501,0.5)
$tcpclient->send("hello world\n");
$redis = new Swoole\Coroutine\Redis();
$redis->connect('127.0.0.1', 6379);
$redis->setDefer();
$redis->get('key');
$mysql = new Swoole\Coroutine\MySQL();
$mysql->connect([
'host' => '127.0.0.1',
'user' => 'user',
'password' => 'pass',
'database' => 'test',
]);
$mysql->setDefer();
$mysql->query('select sleep(1)');
$httpclient = new Swoole\Coroutine\Http\Client('0.0.0.0', 9599);
$httpclient->setHeaders(['Host' => "api.mp.qq.com"]);
$httpclient->set([ 'timeout' => 1]);
$httpclient->setDefer();
$httpclient->get('/');
$tcp_res = $tcpclient->recv();
$redis_res = $redis->recv();
$mysql_res = $mysql->recv();
$http_res = $httpclient->recv();
$response->end('Test End');
});
$server->start();
Swoole2.0基于setjmp
、longjmp
实现,在进行协程切换时会自动保存Zend VM的内存状态(主要是EG全局内存和vm stack)。
$server = new Swoole\Http\Server('127.0.0.1', 9501, SWOOLE_BASE);
#1
$server->on('Request', function($request, $response) {
$mysql = new Swoole\Coroutine\MySQL();
#2
$res = $mysql->connect([
'host' => '127.0.0.1',
'user' => 'root',
'password' => 'root',
'database' => 'test',
]);
#3
if ($res == false) {
$response->end("MySQL connect fail!");
return;
}
$ret = $mysql->query('show tables', 2);
$response->end("swoole response is ok, result=".var_export($ret, true));
});
$server->start();
onRequest
事件回调函数时,底层会调用C函数coro_create
创建一个协程(#1位置),同时保存这个时间点的CPU寄存器状态和ZendVM stack信息。mysql->connect
时发生IO操作,底层会调用C函数coro_save
保存当前协程的状态,包括Zend VM上下文以及协程描述信息,并调用coro_yield
让出程序控制权,当前的请求会挂起(#2位置)core_resume
恢复对应的协程,恢复ZendVM上下文,继续向下执行PHP代码(#3位置)mysql->query
的执行过程与mysql->connect
一致,也会进行一次协程切换调度end
方法返回结果,并销毁此协程相比普通的异步回调程序,协程多增加额外的内存占用。
Ubuntu16.04 + Core I5 4核 + 8G内存 PHP7.0.10
ab -c 100 -n 10000 http://127.0.0.1:9501/
测试结果:
Server Software: swoole-http-server
Server Hostname: 127.0.0.1
Server Port: 9501
Document Path: /
Document Length: 348 bytes
Concurrency Level: 100
Time taken for tests: 0.883 seconds
Complete requests: 10000
Failed requests: 168
(Connect: 0, Receive: 0, Length: 168, Exceptions: 0)
Total transferred: 4914560 bytes
HTML transferred: 3424728 bytes
Requests per second: 11323.69 [#/sec] (mean)
Time per request: 8.831 [ms] (mean)
Time per request: 0.088 [ms] (mean, across all concurrent requests)
Transfer rate: 5434.67 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.2 0 2
Processing: 0 9 9.6 6 96
Waiting: 0 9 9.6 6 96
Total: 0 9 9.6 6 96
Percentage of the requests served within a certain time (ms)
50% 6
66% 9
75% 11
80% 12
90% 19
95% 27
98% 43
99% 51
100% 96 (longest request)