PHP
語言不支持多線程,因此 Swoole
使用多進程模式。
在多進程模式下存在進程內存隔離,解決方案就是使用 MySQL、MongoDB、Redis 等外部存儲服務。
PHP
提供的 MySQL
、CURL
、Redis
等客戶端是同步的,會導致服務器程序發生阻塞。
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
Swoole4
爲PHP
語言提供了強大的CSP
協程編程模式。底層提供了3
個關鍵詞,可以方便地實現各類功能。
go
:創建一個協程chan
:創建一個通道defer
:延遲任務,在協程退出時執行,先進後出
這3
個功能底層現全部爲內存操作,沒有任何IO
資源消耗。就像PHP
的Array
一樣是非常廉價的。如果有需要就可以直接使用。這與socket
和file
操作不同,後者需要向操作系統申請端口和文件描述符,讀寫可能會產生阻塞的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
上述代碼中,test1
和test2
會順序執行,需要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
就可以創建一個通道。通道可以理解爲自帶協程調度的隊列。它有兩個接口push
和pop
:
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.com
和163.com
主頁。協程1
需要拿到Http
請求的結果。這裏使用了chan
來實現併發管理。
- 協程
1
循環兩次對通道進行pop
,因爲隊列爲空,它會進入等待狀態 - 協程
2
和協程3
執行完成後,會push
數據,協程1
拿到了結果,繼續向下執行
延遲任務:defer
在協程編程中,可能需要在協程退出時自動執行一些任務,做清理工作。類似於PHP
的register_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
任務的執行可以延長生命週期 - 默認情況下與
Go
的defer
完全一致,在函數退出時自動執行
使用實例
function test() {
$o = new DeferTask();
//邏輯代碼
$o->add(function () {
//code 1
});
$o->add(function () {
//code 2
});
//函數結束時,對象自動析構,defer 任務自動執行
return $retval;
}