Swoole 快速起步:協程

PHP 語言不支持多線程,因此 Swoole 使用多進程模式。

在多進程模式下存在進程內存隔離,解決方案就是使用 MySQL、MongoDB、Redis 等外部存儲服務。

PHP 提供的 MySQLCURLRedis 等客戶端是同步的,會導致服務器程序發生阻塞。

Swoole 提供了常用的異步客戶端組件,來解決此問題。編寫純異步服務器程序時,可以使用這些異步客戶端。

在最新的Swoole 4.x 版本中移除了這些異步模塊,將使用協程客戶端代替。

使用協程可以以傳統同步編程的方法編寫代碼,底層自動切換爲異步IO,既保證了編程的簡單性,又可藉助異步IO

使用協程客戶端

$http = new swoole_http_server("0.0.0.0", 9501);

$http->on('request', function ($request, $response) {
    $db = new Swoole\Coroutine\MySQL();
    $db->connect([
        'host' => '127.0.0.1',
        'port' => 3306,
        'user' => 'user',
        'password' => 'pass',
        'database' => 'test',
    ]);
    $data = $db->query('select * from test_table');
    $response->end(json_encode($data));
});

$http->start();

上面的代碼編寫與同步阻塞模式的程序完全一致的。但是底層自動進行了協程切換處理,變爲異步IO

協程:併發 shell_exec

PHP程序中經常需要用shell_exec執行一些命令,而普通的shell_exec是阻塞的,如果命令執行時間過長,那可能會導致進程完全卡住。 在Swoole4協程環境下可以用Co::exec併發地執行很多命令。

併發 shell_exec

<?php
$c = 10;
while($c--) {
    go(function () {
        //這裏使用 sleep 5 來模擬一個很長的命令
        co::exec("sleep 5");
    });
}

執行結果:

$ time php t.php

real    0m5.089s
user    0m0.067s
sys 0m0.038s

 阻塞代碼

<?php
$c = 10;
while($c--) {
    //這裏使用 sleep 5 來模擬一個很長的命令
    shell_exec("sleep 5");
}

執行結果:

$ time php s.php 

real    0m50.119s
user    0m0.066s
sys 0m0.058s

協程:Go

Swoole4PHP語言提供了強大的CSP協程編程模式。底層提供了3個關鍵詞,可以方便地實現各類功能。

  • go :創建一個協程
  • chan :創建一個通道
  • defer :延遲任務,在協程退出時執行,先進後出

3個功能底層現全部爲內存操作,沒有任何IO資源消耗。就像PHPArray一樣是非常廉價的。如果有需要就可以直接使用。這與socketfile操作不同,後者需要向操作系統申請端口和文件描述符,讀寫可能會產生阻塞的IO等待。

順序執行

function test1() 
{
    sleep(1);
    echo "b";
}

function test2() 
{
    sleep(2);
    echo "c";
}

test1();
test2();

結果:

$ time php b1.php
bc
real    0m3.080s
user    0m0.016s
sys     0m0.063s

上述代碼中,test1test2會順序執行,需要3秒才能執行完成。

使用go創建協程

Swoole\Runtime::enableCoroutine();

go(function () 
{
    sleep(1);
    echo "b";
});

go(function () 
{
    sleep(2);
    echo "c";
});

結果: 

$ time php co.php
bc
real    0m2.076s
user    0m0.000s
sys     0m0.078s

可以看到這裏只用了2秒就執行完成了。順序執行耗時等於所有任務執行耗時的總和 :t1+t2+t3...,併發執行耗時等於所有任務執行耗時的最大值 :max(t1, t2, t3, ...);

協程通信: Chan

協程併發執行,另外一個協程,需要依賴這兩個協程的執行結果,如果解決此問題呢?

答案就是使用通道(Channel),在Swoole4協程中使用new chan就可以創建一個通道。通道可以理解爲自帶協程調度的隊列。它有兩個接口pushpop

  • push:向通道中寫入內容,如果已滿,它會進入等待狀態,有空間時自動恢復
  • pop:從通道中讀取內容,如果爲空,它會進入等待狀態,有數據時自動恢復

使用通道實現併發管理:

$chan = new chan(2);

# 協程1
go (function () use ($chan) {
    $result = [];
    for ($i = 0; $i < 2; $i++)
    {
        $result += $chan->pop();
    }
    var_dump($result);
});

# 協程2
go(function () use ($chan) {
   $cli = new Swoole\Coroutine\Http\Client('www.qq.com', 80);
       $cli->set(['timeout' => 10]);
       $cli->setHeaders([
       'Host' => "www.qq.com",
       "User-Agent" => 'Chrome/49.0.2587.3',
       'Accept' => 'text/html,application/xhtml+xml,application/xml',
       'Accept-Encoding' => 'gzip',
   ]);
   $ret = $cli->get('/');
   // $cli->body 響應內容過大,這裏用 Http 狀態碼作爲測試
   $chan->push(['www.qq.com' => $cli->statusCode]);
});

# 協程3
go(function () use ($chan) {
   $cli = new Swoole\Coroutine\Http\Client('www.163.com', 80);
   $cli->set(['timeout' => 10]);
   $cli->setHeaders([
       'Host' => "www.163.com",
       "User-Agent" => 'Chrome/49.0.2587.3',
       'Accept' => 'text/html,application/xhtml+xml,application/xml',
       'Accept-Encoding' => 'gzip',
   ]);
   $ret = $cli->get('/');
   // $cli->body 響應內容過大,這裏用 Http 狀態碼作爲測試
   $chan->push(['www.163.com' => $cli->statusCode]);
});

執行結果:

htf@LAPTOP-0K15EFQI:~/swoole-src/examples/5.0$ time php co2.php
array(2) {
  ["www.qq.com"]=>
  int(302)
  ["www.163.com"]=>
  int(200)
}

real    0m0.268s
user    0m0.016s
sys     0m0.109s
htf@LAPTOP-0K15EFQI:~/swoole-src/examples/5.0$

這裏使用go創建了3個協程,協程2和協程3分別請求qq.com163.com主頁。協程1需要拿到Http請求的結果。這裏使用了chan來實現併發管理。

  • 協程1循環兩次對通道進行pop,因爲隊列爲空,它會進入等待狀態
  • 協程2和協程3執行完成後,會push數據,協程1拿到了結果,繼續向下執行

延遲任務:defer

在協程編程中,可能需要在協程退出時自動執行一些任務,做清理工作。類似於PHPregister_shutdown_function

Swoole4中使用defer實現:

Swoole\Runtime::enableCoroutine();

go(function () {
    echo "a";
    defer(function () {
        echo "~a";
    });
    echo "b";
    defer(function () {
        echo "~b";
    });
    sleep(1);
    echo "c";
});

執行結果:

$ time php defer.php
abc~b~a
real    0m1.068s
user    0m0.016s
sys     0m0.047s
htf@LAPTOP-0K15EFQI:~/swoole-src/examples/5.0$

協程:實現 Go 語言風格的 defer

由於Go語言沒有提供析構方法,而PHP對象有析構函數,使用__destruct就可以實現Go的風格defer

實現代碼:

class DeferTask
{
    private $tasks;

    function add(callable $fn)
    {
        $this->tasks[] = $fn;
    }

    function __destruct()
    {
        //反轉
        $tasks = array_reverse($this->tasks);
        foreach($tasks as $fn)
        {
            $fn();
        }
    }
}
  • 基於PHP對象析構方法實現的defer更靈活,如果希望改變執行的時機,甚至可以將DeferTask對象賦值給其他生命週期更長的變量,defer任務的執行可以延長生命週期
  • 默認情況下與Godefer完全一致,在函數退出時自動執行

使用實例

function test() {
    $o = new DeferTask();
    //邏輯代碼
    $o->add(function () {
        //code 1
    });
    $o->add(function () {
        //code 2
    });
    //函數結束時,對象自動析構,defer 任務自動執行
    return $retval;
}

官網原文地址:https://wiki.swoole.com/wiki/page/1005.html

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