php為什么需要異步編程?php異步編程的詳解(附示例)


本篇文章給大家帶來的內容是關於php為什么需要異步編程?php異步編程的詳解(附示例),有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。

我對 php 異步的知識還比較混亂,寫這篇是為了整理,可能有錯。

傳統的 php-fpm 一個進程執行一個請求,要達到多少並發,就要生成多少個進程。更糟糕的是每次請求都需要重新編譯執行,導致並發一直上不來。因此出現了 Swoole 和 WorkerMan 兩個國內流行的常駐內存框架[1]。這兩個框架原理都是通過事件循環,讓程序一直停留在內存,等待外部請求,達到高並發。

為什么需要異步

先來看一個例子

在工作目錄下新建文件 slowServer.php

1

2

3

<?php

sleep(5); // 5秒后才能返回請求

echo 'done';

開啟服務

1

$ php -S localhost:8081 slowServer.php

開另一個終端,安裝依賴

1

2

3

$ pecl install event # 安裝 event 擴展

$ composer require workerman/workerman

$ composer require react/http-client:^0.5.9

新建文件 worker.php

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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();

開啟服務器

1

php worker.php start

在瀏覽器開啟兩個標簽,都打開網址 http://localhost:8082 。這時可以看到終端輸出“1”,過了一會兒又輸出“1”,原因是8081服務器在處理第一個請求的時候阻塞在了等待8081返回之中,等第一個請求結束后,才開始處理第二個請求。也就是說請求是一個一個執行的,要達到多少個並發,就要建立多少個進程,跟 php-fpm 一樣。現在修改一下代碼

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

$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 等待。

異步的意義在於把 cpu 從 io 等待中解放出來,可以處理其他計算任務。 如果你想知道怎么用框架實現異步,看到這里就可以了。WorkerMan 配合 ReactPHP 或者自身的 AsyncTcpConnection 已經可以滿足很多 io 請求異步化的需求。下面繼續討論這些框架是怎么做到異步的。

哪些地方應該被做成異步

通過上面的例子已經知道一旦執行到不需要 cpu,但是要等待 io 的時候,應該把 io 的過程做成異步。

實現事件循環

上面的例子是通過 reactphp 把 http 請求變成了異步,其實 WorkerMan 框架本身也是異步的,下面來看看 WorkerMan 是怎么使 onMessage 函數可以異步接受請求。先來新建下面這個文件 react.php

1

2

3

4

5

6

7

8

9

10

11

12

13

<?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(); // 開始循環

開始執行

1

$ php react.php

在另一個終端執行

1

telnet 127.0.0.1 8081

這時就會看到第一個終端輸出'1'。

我之前寫過一篇文章《php使用epoll》,是這篇文章的基礎。那篇文章里事件回調是通過定時來實現,即

1

$event->add($seconds);

而這里,事件回調是通過檢測 fd 是否有寫入內容來實現,這個過程不需要 cpu 參與。當 fd 有內容寫入時,會調函數 'react',這時開始使用 cpu。如果這時候進程執行另一個異步請求,比如用 reactphp 框架請求一個網頁,那么程序會讓出 cpu,此時如果有另一個請求進來,就可以回調執行另一個 'react' 函數。由此提高了並發量。

協程

生成器 Generater

這是生成器的 PHP 官方文檔 http://php.net/manual/zh/lang...

1

2

3

4

5

6

7

8

9

10

11

12

<?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 里的循環,取決於主程序是否繼續調用

什么是協程

上面的程序另一種寫法是

1

2

3

4

5

6

7

8

9

10

11

12

<?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 兩個框架封裝了協程,有興趣可以了解一下。

明確的學習思路能更高效的學習

 

點擊加入該群學習


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM