Swoole Coroutine協(xié)程支持

2022-07-12 11:26 更新

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ì):

  1. 開發(fā)者可以無(wú)感知的用同步的代碼編寫方式達(dá)到異步IO的效果和性能,避免了傳統(tǒng)異步回調(diào)所帶來(lái)的離散的代碼邏輯和陷入多層回調(diào)中導(dǎo)致代碼無(wú)法維護(hù)。

  2. 同時(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的封裝,包括:

  • UDP
  • TCP
  • HTTP
  • Mysql
  • Redis

可以滿足大部分開發(fā)者的需求。對(duì)于私有協(xié)議,開發(fā)者可以使用協(xié)程的TCP或者UDP接口去方便的封裝。

啟用

Prerequisite:

  • PHP版本要求:>= 5.5,包括5.5、5.6、7.0、7.1
  • 基于swoole_server或者swoole_http_server進(jìn)行開發(fā),目前只支持在onRequetonReceiveonConnect事件回調(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_serverswoole_http_server將以為每一個(gè)請(qǐng)求創(chuàng)建對(duì)應(yīng)的協(xié)程,開發(fā)者可以在onRequet、onReceive、onConnect 3個(gè)事件回調(diào)中使用協(xié)程客戶端。

相關(guān)配置

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的文檔。

注意事項(xiàng)

  1. 全局變量:協(xié)程使得原有的異步邏輯同步化,但是在協(xié)程的切換是隱式發(fā)生的,所以在協(xié)程切換的前后不能保證全局變量以及static變量的一致性。
  2. 請(qǐng)勿在以下場(chǎng)景中觸發(fā)協(xié)程切換:
    • 析構(gòu)函數(shù)
    • 魔術(shù)方法__call()
  3. gcc 4.4下如果在編譯swoole的時(shí)候(即make階段),出現(xiàn)gcc warning: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
  4. 與xdebug、xhprof等zend擴(kuò)展不兼容,例如不能使用xhprof對(duì)協(xié)程server進(jìn)行性能分析采樣。
  5. 在PHP5中,原生的call_user_func和call_user_func_array中無(wú)法使用協(xié)程client,請(qǐng)使用\Swoole\Coroutine::call_user_func和\Swoole\Coroutine::call_user_func_array代替
  6. 在PHP7中可直接調(diào)用原生的call_user_func和call_user_func_array

方法列表

getDefer()

bool getDefer();
  • 返回值:返回當(dāng)前設(shè)置的defer

setDefer()

bool setDefer([bool $is_defer = true]);
  • $is_defer:bool值,為true時(shí),表明該Client要延遲收包,為false時(shí),表明該Client非延遲收包,默認(rèn)值為true
  • 返回值:設(shè)置成功返回true,否則返回false。只有一種情況會(huì)返回false,當(dāng)設(shè)置defer(true)并發(fā)包后,尚未recv()收包,就設(shè)置defer(false),此時(shí)返回false。
  • 如果需要進(jìn)行延遲收包,需要在發(fā)包之前調(diào)用

recv()

mixed recv();
  • 返回值:獲取延遲收包的結(jié)果,當(dāng)沒有進(jìn)行延遲收包或者收包超時(shí),返回false。

并發(fā)調(diào)用

Client并發(fā)請(qǐng)求


在協(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
  • Swoole\Coroutine\Redis
  • Swoole\Coroutine\MySQL
  • Swoole\Coroutine\Http\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();

實(shí)現(xiàn)原理

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();
  • 此程序僅啟動(dòng)了一個(gè)1個(gè)進(jìn)程,就可以并發(fā)處理大量請(qǐng)求。
  • 程序的性能基本上與異步回調(diào)方式相同,但是代碼完全是同步編寫的

運(yùn)行過(guò)程

  • 調(diào)用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信息。
  • 調(diào)用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位置)
  • 協(xié)程讓出程序控制權(quán)后,會(huì)繼續(xù)進(jìn)入EventLoop處理其他事件,這時(shí)Swoole會(huì)繼續(xù)去處理其他客戶端發(fā)來(lái)的Request
  • IO事件完成后,MySQL連接成功或失敗,底層調(diào)用C函數(shù)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)度
  • 所有操作完成后,調(diào)用end方法返回結(jié)果,并銷毀此協(xié)程

協(xié)程開銷

相比普通的異步回調(diào)程序,協(xié)程多增加額外的內(nèi)存占用。

  • Swoole2.0協(xié)程需要為每個(gè)并發(fā)保存zend stack棧內(nèi)存并維護(hù)對(duì)應(yīng)的虛擬機(jī)狀態(tài)。如果程序并發(fā)很大可能會(huì)占用大量?jī)?nèi)存,取決于C函數(shù)、ZendVM 調(diào)用棧深度
  • 協(xié)程調(diào)度會(huì)增加額外的一些CPU開銷

壓力測(cè)試

  • 環(huán)境: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)


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)