Swoole在2.0開始內(nèi)置協(xié)程(Coroutine)的能力,提供了具備協(xié)程能力IO接口(統(tǒng)一在名空間Swoole\Coroutine\*
)。
2.0.2或更高版本已支持PHP7
協(xié)程可以理解為純用戶態(tài)的線程,其通過(guò)協(xié)作而不是搶占來(lái)進(jìn)行切換。相對(duì)于進(jìn)程或者線程,協(xié)程所有的操作都可以在用戶態(tài)完成,創(chuàng)建和切換的消耗更低。Swoole可以為每一個(gè)請(qǐng)求創(chuàng)建對(duì)應(yīng)的協(xié)程,根據(jù)IO的狀態(tài)來(lái)合理的調(diào)度協(xié)程,這會(huì)帶來(lái)了以下優(yōu)勢(shì):
開發(fā)者可以無(wú)感知的用同步的代碼編寫方式達(dá)到異步IO的效果和性能,避免了傳統(tǒng)異步回調(diào)所帶來(lái)的離散的代碼邏輯和陷入多層回調(diào)中導(dǎo)致代碼無(wú)法維護(hù)。
同時(shí)由于swoole是在底層封裝了協(xié)程,所以對(duì)比傳統(tǒng)的php層協(xié)程框架,開發(fā)者不需要使用yield關(guān)鍵詞來(lái)標(biāo)識(shí)一個(gè)協(xié)程IO操作,所以不再需要對(duì)yield的語(yǔ)義進(jìn)行深入理解以及對(duì)每一級(jí)的調(diào)用都修改為yield,這極大的提高了開發(fā)效率。
協(xié)程API目前針對(duì)了TCP,UDP等主流協(xié)議client的封裝,包括:
可以滿足大部分開發(fā)者的需求。對(duì)于私有協(xié)議,開發(fā)者可以使用協(xié)程的TCP或者UDP接口去方便的封裝。
swoole_server
或者swoole_http_server
進(jìn)行開發(fā),目前只支持在onRequet
, onReceive
, onConnect
事件回調(diào)函數(shù)中使用協(xié)程。swoole2.0需要通過(guò)添加--enable-coroutine
編譯參數(shù)啟用協(xié)程能力,示例如下:
phpize
./configure --with-php-config={path-to-php-config} --enable-coroutine
make
make install
添加編譯參數(shù),swoole server將切換到協(xié)程模式。
開啟協(xié)程模式后,swoole_server
和swoole_http_server
將以為每一個(gè)請(qǐng)求創(chuàng)建對(duì)應(yīng)的協(xié)程,開發(fā)者可以在onRequet
、onReceive
、onConnect
3個(gè)事件回調(diào)中使用協(xié)程客戶端。
在Swoole\Server
的set方法中增加了一個(gè)配置參數(shù)max_coro_num
,用于配置一個(gè)worker進(jìn)程最多同時(shí)處理的協(xié)程數(shù)目。因?yàn)殡S著worker進(jìn)程處理的協(xié)程數(shù)目的增加,其占用的內(nèi)存也會(huì)增加,為了避免超出php的memory_limit
限制,請(qǐng)根據(jù)實(shí)際業(yè)務(wù)的壓測(cè)結(jié)果設(shè)置該值,默認(rèn)為3000。
當(dāng)代碼執(zhí)行到connect()和recv()
函數(shù)時(shí),swoole會(huì)觸發(fā)進(jìn)行協(xié)程切換,此時(shí)swoole可以去處理其他的事件或者接受新的請(qǐng)求。當(dāng)此client連接
成功或者后端服務(wù)回包
后,swoole server會(huì)恢復(fù)協(xié)程上下文,代碼邏輯繼續(xù)從切換點(diǎn)開始恢復(fù)執(zhí)行。開發(fā)者整個(gè)過(guò)程不需要關(guān)心整個(gè)切換過(guò)程。具體使用可以參考client的文檔。
__call()
dereferencing pointer ‘v.327’ does break strict-aliasing rules
、dereferencing type-punned pointer will break strict-aliasing rules
請(qǐng)手動(dòng)編輯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();
在協(xié)程版本的Client中,實(shí)現(xiàn)了多個(gè)客戶端并發(fā)的發(fā)包功能。
通常,如果一個(gè)業(yè)務(wù)請(qǐng)求中需要做一次redis請(qǐng)求和一次mysql請(qǐng)求,那么網(wǎng)絡(luò)IO會(huì)是這樣子:
redis發(fā)包->redis收包->mysql發(fā)包->mysql收包
以上流程網(wǎng)絡(luò)IO的時(shí)間就等于 redis網(wǎng)絡(luò)IO時(shí)間 + mysql網(wǎng)絡(luò)IO時(shí)間。
而對(duì)于協(xié)程版本的Client,網(wǎng)絡(luò)IO可以是這樣子:
redis發(fā)包->mysql發(fā)包->redis收包->mysql收包
以上流程網(wǎng)絡(luò)IO的時(shí)間就接近于 MAX(redis網(wǎng)絡(luò)IO時(shí)間, mysql網(wǎng)絡(luò)IO時(shí)間)。
現(xiàn)在支持并發(fā)請(qǐng)求的Client有:
除了Swoole\Coroutine\Client,其他Client都實(shí)現(xiàn)了defer特性,用于聲明延遲收包。
因?yàn)镾woole\Coroutine\Client的發(fā)包和收包方法是分開的,所以就不需要實(shí)現(xiàn)defer特性了,而其他Client的發(fā)包和收包都是在一個(gè)方法中,所以需要一個(gè)setDefer()方法聲明延遲收包,然后通過(guò)recv()方法收包。
協(xié)程版本Client并發(fā)請(qǐng)求示例代碼:
<?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
實(shí)現(xiàn),在進(jìn)行協(xié)程切換時(shí)會(huì)自動(dòng)保存Zend VM的內(nèi)存狀態(tài)(主要是EG全局內(nèi)存和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
事件回調(diào)函數(shù)時(shí),底層會(huì)調(diào)用C函數(shù)coro_create
創(chuàng)建一個(gè)協(xié)程(#1位置),同時(shí)保存這個(gè)時(shí)間點(diǎn)的CPU寄存器狀態(tài)和ZendVM stack信息。mysql->connect
時(shí)發(fā)生IO操作,底層會(huì)調(diào)用C函數(shù)coro_save
保存當(dāng)前協(xié)程的狀態(tài),包括Zend VM上下文以及協(xié)程描述信息,并調(diào)用coro_yield
讓出程序控制權(quán),當(dāng)前的請(qǐng)求會(huì)掛起(#2位置)core_resume
恢復(fù)對(duì)應(yīng)的協(xié)程,恢復(fù)ZendVM上下文,繼續(xù)向下執(zhí)行PHP代碼(#3位置)mysql->query
的執(zhí)行過(guò)程與mysql->connect
一致,也會(huì)進(jìn)行一次協(xié)程切換調(diào)度end
方法返回結(jié)果,并銷毀此協(xié)程相比普通的異步回調(diào)程序,協(xié)程多增加額外的內(nèi)存占用。
Ubuntu16.04 + Core I5 4核 + 8G內(nèi)存 PHP7.0.10
ab -c 100 -n 10000 http://127.0.0.1:9501/
測(cè)試結(jié)果:
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)
更多建議: