swoole深入學習 8. 協程2

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

在新版本的中,在不支持協程的位置可以使用goCo::create創建協程。這些內容我會在下節中會單獨講。

Swoole\Coroutine\Client

Swoole\Coroutine\Client 提供了TCP和UDP傳輸協議Socket客戶端的封裝代碼,使用時僅需new Swoole\Coroutine\Client即可。

直接看例子吧,我在swoole\http\serveronRequest裏去調用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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章