前言
我對 php 非同步的知識還比較混亂,寫這篇是為了整理,可能有錯。
傳統的 php-fpm 一個程式執行一個請求,要達到多少併發,就要生成多少個程式。更糟糕的是每次請求都需要重新編譯執行,導致併發一直上不來。因此出現了 Swoole 和 WorkerMan 兩個國內流行的常駐記憶體框架[1]。這兩個框架原理都是通過事件迴圈,讓程式一直停留在記憶體,等待外部請求,達到高併發。
為什麼需要非同步
先來看一個例子
在工作目錄下新建檔案 slowServer.php
<?php
sleep(5); // 5秒後才能返回請求
echo `done`;
開啟服務
$ php -S localhost:8081 slowServer.php
開另一個終端,安裝依賴
$ pecl install event # 安裝 event 擴充套件
$ composer require workerman/workerman
$ composer require react/http-client:^0.5.9
新建檔案 worker.php
require_once __DIR__ . `/vendor/autoload.php`;
use WorkermanWorker;
use WorkermanConnectionAsyncTcpConnection;
use AmpArtaxResponse;
$http_worker = new Worker("http://0.0.0.0:8082");
$http_worker->count = 1; // 只開一個程式
$http_worker->onMessage = function($connection, $host) {
echo 1;
$data = file_get_contents(`http://localhost:8081`);
$connection->send($data);
};
Worker::runAll();
開啟伺服器
php worker.php start
在瀏覽器開啟兩個標籤,都開啟網址 http://localhost:8082 。這時可以看到終端輸出“1”,過了一會兒又輸出“1”,原因是8081伺服器在處理第一個請求的時候阻塞在了等待8081返回之中,等第一個請求結束後,才開始處理第二個請求。也就是說請求是一個一個執行的,要達到多少個併發,就要建立多少個程式,跟 php-fpm 一樣。現在修改一下程式碼
$http_worker->onMessage = function($connection, $host) {
echo 1;
$loop = Worker::getEventLoop();
$client = new ReactHttpClientClient($loop);
$request = $client->request(`GET`, `http://localhost:8081`);
$request->on(`error`, function(Exception $e) use ($connection) {
$connection->send($e);
});
$request->on(`response`, function ($response) use ($connection) {
$response->on(`data`, function ($data) use ($connection) {
$connection->send($data);
});
});
$request->end();
};
現在開啟服務,再在瀏覽器發起請求,發現第二個“1”在請求後就馬上輸出了,而這時第一個請求還沒結束。這表明程式不再阻塞,併發量取決於 cpu 和 記憶體,而不是程式數。
為什麼需要非同步
通過上面的例子已經很明白了,reactphp 框架通過把 http 請求變成非同步,讓 onMessage 函式變成非阻塞,cpu 可以去處理下一個請求。即從 cpu 迴圈等待 8081 返回,變成了 epoll 等待。
非同步的意義在於把 cpu 從 io 等待中解放出來,可以處理其他計算任務。 如果你想知道怎麼用框架實現非同步,看到這裡就可以了。WorkerMan 配合 ReactPHP 或者自身的 AsyncTcpConnection 已經可以滿足很多 io 請求非同步化的需求。下面繼續討論這些框架是怎麼做到非同步的。
哪些地方應該被做成非同步
通過上面的例子已經知道一旦執行到不需要 cpu,但是要等待 io 的時候,應該把 io 的過程做成非同步。
實現事件迴圈
上面的例子是通過 reactphp 把 http 請求變成了非同步,其實 WorkerMan 框架本身也是非同步的,下面來看看 WorkerMan 是怎麼使 onMessage 函式可以非同步接受請求。先來新建下面這個檔案 react.php
<?php
$context = stream_context_create();
$socket = stream_socket_server(`tcp://0.0.0.0:8081`, $errno, $errmsg, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,$context); // 註冊一個 fd(file descriptor)
function react($socket){
$new_socket = stream_socket_accept($socket, 0, $remote_address);
echo 1;
}
$eventBase = new EventBase();
$event = new Event($eventBase, $socket, Event::READ | Event::PERSIST, `react`, $socket); // 註冊一個事件,檢測 fd 有沒有寫入內容
$event->add();
$eventBase->loop(); // 開始迴圈
開始執行
$ php react.php
在另一個終端執行
telnet 127.0.0.1 8081
這時就會看到第一個終端輸出`1`。
我之前寫過一篇文章《php使用epoll》,是這篇文章的基礎。那篇文章裡事件回撥是通過定時來實現,即
$event->add($seconds);
而這裡,事件回撥是通過檢測 fd 是否有寫入內容來實現,這個過程不需要 cpu 參與。當 fd 有內容寫入時,會調函式 `react`,這時開始使用 cpu。如果這時候程式執行另一個非同步請求,比如用 reactphp 框架請求一個網頁,那麼程式會讓出 cpu,此時如果有另一個請求進來,就可以回撥執行另一個 `react` 函式。由此提高了併發量。
協程
生成器 Generater
這是生成器的 PHP 官方文件 http://php.net/manual/zh/lang…
<?php
function gen_one_to_three() {
for ($i = 1; $i <= 3; $i++) {
//注意變數$i的值在不同的yield之間是保持傳遞的。
yield $i;
}
}
$generator = gen_one_to_three();
foreach ($generator as $value) {
echo "$value
";
}
生成器就是每次程式執行到 yield 的時候儲存狀態,然後返回 $i,是否繼續執行 gen_one_to_three 裡的迴圈,取決於主程式是否繼續呼叫
什麼是協程
上面的程式另一種寫法是
<?php
$i = 1;
function gen_one_to_three() {
global $i;
if ($i<=3){
return $i++;
}
}
while ($value = gen_one_to_three()) {
echo "$value
";
}
由此可見,協程就是一種對函式的封裝,使其變成一種可以被中斷的函式,行為更像是子程式或子執行緒,而不是函式。協程的具體寫法這裡不細寫,因為協程的寫法十分複雜,可能需要再做一層封裝才能好用。
協程與非同步
既然協程可以被中斷,那麼只要在程式發起請求後發起事件迴圈,然後用 yield 返回,然後程式繼續執行主程式部分,等事件返回後觸發函式,執行 Generatot::next() 或 Generator::send() 來繼續執行協程部分。封裝好後就好像沒有非同步回撥函式一樣,和同步函式很像。
現在已經有 ampphp 和 swoole 兩個框架封裝了協程,有興趣可以瞭解一下。
- 國外還有 https://amphp.org 和 https://reactphp.org 這兩個框架
部落格地址:http://b.ljj.pub