我對 php 異步的知識還比較混亂,寫這篇是爲了整理,可能有錯。php
傳統的 php-fpm 一個進程執行一個請求,要達到多少併發,就要生成多少個進程。更糟糕的是每次請求都須要從新編譯執行,致使併發一直上不來。所以出現了 Swoole 和 WorkerMan 兩個國內流行的常駐內存框架[1]。這兩個框架原理都是經過事件循環,讓程序一直停留在內存,等待外部請求,達到高併發。react
在工做目錄下新建文件 slowServer.php瀏覽器
<?php sleep(5); // 5秒後才能返回請求 echo 'done';
開啓服務服務器
$ php -S localhost:8081 slowServer.php
開另外一個終端,安裝依賴swoole
$ 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 Workerman\Worker; use Workerman\Connection\AsyncTcpConnection; use Amp\Artax\Response; $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();
開啓服務器composer
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 \React\HttpClient\Client($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 等待。socket
異步的意義在於把 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' 函數。由此提升了併發量。
這是生成器的 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\n"; }
生成器就是每次程序執行到 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\n"; }
因而可知,協程就是一種對函數的封裝,使其變成一種能夠被中斷的函數,行爲更像是子進程或子線程,而不是函數。協程的具體寫法這裏不細寫,由於協程的寫法十分複雜,可能須要再作一層封裝才能好用。
既然協程能夠被中斷,那麼只要在程序發起請求後發起事件循環,而後用 yield 返回,而後程序繼續執行主程序部分,等事件返回後觸發函數,執行 Generatot::next() 或 Generator::send() 來繼續執行協程部分。封裝好後就好像沒有異步回調函數同樣,和同步函數很像。
如今已經有 ampphp 和 swoole 兩個框架封裝了協程,有興趣能夠了解一下。
博客地址:http://b.ljj.pub