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)的線程,其通過協(xié)作而不是搶占來進行切換。相對于進程或者線程,協(xié)程所有的操作都可以在用戶態(tài)完成,創(chuàng)建和切換的消耗更低。Swoole可以為每一個請求創(chuàng)建對應的協(xié)程,根據(jù)IO的狀態(tài)來合理的調(diào)度協(xié)程,這會帶來了以下優(yōu)勢:

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

  2. 同時由于swoole是在底層封裝了協(xié)程,所以對比傳統(tǒng)的php層協(xié)程框架,開發(fā)者不需要使用yield關鍵詞來標識一個協(xié)程IO操作,所以不再需要對yield的語義進行深入理解以及對每一級的調(diào)用都修改為yield,這極大的提高了開發(fā)效率。

協(xié)程API目前針對了TCP,UDP等主流協(xié)議client的封裝,包括:

  • UDP
  • TCP
  • HTTP
  • Mysql
  • Redis

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

啟用

Prerequisite:

  • PHP版本要求:>= 5.5,包括5.5、5.6、7.0、7.1
  • 基于swoole_server或者swoole_http_server進行開發(fā),目前只支持在onRequetonReceiveonConnect事件回調(diào)函數(shù)中使用協(xié)程。

swoole2.0需要通過添加--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將以為每一個請求創(chuàng)建對應的協(xié)程,開發(fā)者可以在onRequet、onReceive、onConnect 3個事件回調(diào)中使用協(xié)程客戶端。

相關配置

Swoole\Server的set方法中增加了一個配置參數(shù)max_coro_num,用于配置一個worker進程最多同時處理的協(xié)程數(shù)目。因為隨著worker進程處理的協(xié)程數(shù)目的增加,其占用的內(nèi)存也會增加,為了避免超出php的memory_limit限制,請根據(jù)實際業(yè)務的壓測結(jié)果設置該值,默認為3000。

使用示例


當代碼執(zhí)行到connect()和recv()函數(shù)時,swoole會觸發(fā)進行協(xié)程切換,此時swoole可以去處理其他的事件或者接受新的請求。當此client連接成功或者后端服務回包后,swoole server會恢復協(xié)程上下文,代碼邏輯繼續(xù)從切換點開始恢復執(zhí)行。開發(fā)者整個過程不需要關心整個切換過程。具體使用可以參考client的文檔。

注意事項

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

setDefer()

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

recv()

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

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

Client并發(fā)請求


在協(xié)程版本的Client中,實現(xiàn)了多個客戶端并發(fā)的發(fā)包功能。

通常,如果一個業(yè)務請求中需要做一次redis請求和一次mysql請求,那么網(wǎng)絡IO會是這樣子:

redis發(fā)包->redis收包->mysql發(fā)包->mysql收包

以上流程網(wǎng)絡IO的時間就等于 redis網(wǎng)絡IO時間 + mysql網(wǎng)絡IO時間。

而對于協(xié)程版本的Client,網(wǎng)絡IO可以是這樣子:

redis發(fā)包->mysql發(fā)包->redis收包->mysql收包

以上流程網(wǎng)絡IO的時間就接近于 MAX(redis網(wǎng)絡IO時間, mysql網(wǎng)絡IO時間)。

現(xiàn)在支持并發(fā)請求的Client有:

  • Swoole\Coroutine\Client
  • Swoole\Coroutine\Redis
  • Swoole\Coroutine\MySQL
  • Swoole\Coroutine\Http\Client

除了Swoole\Coroutine\Client,其他Client都實現(xiàn)了defer特性,用于聲明延遲收包。

因為Swoole\Coroutine\Client的發(fā)包和收包方法是分開的,所以就不需要實現(xiàn)defer特性了,而其他Client的發(fā)包和收包都是在一個方法中,所以需要一個setDefer()方法聲明延遲收包,然后通過recv()方法收包。


協(xié)程版本Client并發(fā)請求示例代碼:

<?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();

實現(xiàn)原理

Swoole2.0基于setjmp、longjmp實現(xiàn),在進行協(xié)程切換時會自動保存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();
  • 此程序僅啟動了一個1個進程,就可以并發(fā)處理大量請求。
  • 程序的性能基本上與異步回調(diào)方式相同,但是代碼完全是同步編寫的

運行過程

  • 調(diào)用onRequest事件回調(diào)函數(shù)時,底層會調(diào)用C函數(shù)coro_create創(chuàng)建一個協(xié)程(#1位置),同時保存這個時間點的CPU寄存器狀態(tài)和ZendVM stack信息。
  • 調(diào)用mysql->connect時發(fā)生IO操作,底層會調(diào)用C函數(shù)coro_save保存當前協(xié)程的狀態(tài),包括Zend VM上下文以及協(xié)程描述信息,并調(diào)用coro_yield讓出程序控制權(quán),當前的請求會掛起(#2位置)
  • 協(xié)程讓出程序控制權(quán)后,會繼續(xù)進入EventLoop處理其他事件,這時Swoole會繼續(xù)去處理其他客戶端發(fā)來的Request
  • IO事件完成后,MySQL連接成功或失敗,底層調(diào)用C函數(shù)core_resume恢復對應的協(xié)程,恢復ZendVM上下文,繼續(xù)向下執(zhí)行PHP代碼(#3位置)
  • mysql->query的執(zhí)行過程與mysql->connect一致,也會進行一次協(xié)程切換調(diào)度
  • 所有操作完成后,調(diào)用end方法返回結(jié)果,并銷毀此協(xié)程

協(xié)程開銷

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

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

壓力測試

  • 環(huán)境:Ubuntu16.04 + Core I5 4核 + 8G內(nèi)存 PHP7.0.10
  • 腳本:ab -c 100 -n 10000 http://127.0.0.1:9501/

測試結(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)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號