當被swoole協(xié)程三連問時,快哭了!

swoole教程介紹相關(guān)協(xié)程的面試問題當被swoole協(xié)程三連問時,快哭了!

推薦(免費):swoole教程

什么是進程?

進程就是應(yīng)用程序的啟動實例。獨立的文件資源,數(shù)據(jù)資源,內(nèi)存空間。

什么是線程?

線程屬于進程,是程序的執(zhí)行者。一個進程至少包含一個主線程,也可以有更多的子線程。線程有兩種調(diào)度策略,一是:分時調(diào)度,二是:搶占式調(diào)度。

我的官方企鵝群

什么是協(xié)程?

協(xié)程是輕量級線程,協(xié)程也是屬于線程,協(xié)程是在線程里執(zhí)行的。協(xié)程的調(diào)度是用戶手動切換的,所以又叫用戶空間線程。協(xié)程的創(chuàng)建、切換、掛起、銷毀全部為內(nèi)存操作,消耗是非常低的。協(xié)程的調(diào)度策略是:協(xié)作式調(diào)度。

Swoole 協(xié)程的原理

  • Swoole4 由于是單線程多進程的,同一時間同一個進程只會有一個協(xié)程在運行。

  • Swoole server 接收數(shù)據(jù)在 worker 進程觸發(fā) onReceive 回調(diào),產(chǎn)生一個攜程。Swoole 為每個請求創(chuàng)建對應(yīng)攜程。協(xié)程中也能創(chuàng)建子協(xié)程。

  • 協(xié)程在底層實現(xiàn)上是單線程的,因此同一時間只有一個協(xié)程在工作,協(xié)程的執(zhí)行是串行的。

  • 因此多任務(wù)多協(xié)程執(zhí)行時,一個協(xié)程正在運行時,其他協(xié)程會停止工作。當前協(xié)程執(zhí)行阻塞 IO 操作時會掛起,底層調(diào)度器會進入事件循環(huán)。當有 IO 完成事件時,底層調(diào)度器恢復(fù)事件對應(yīng)的協(xié)程的執(zhí)行。。所以協(xié)程不存在 IO 耗時,非常適合高并發(fā) IO 場景。(如下圖)

當被swoole協(xié)程三連問時,快哭了!

Swoole 的協(xié)程執(zhí)行流程

  • 協(xié)程沒有 IO 等待 正常執(zhí)行 PHP 代碼,不會產(chǎn)生執(zhí)行流程切換

  • 協(xié)程遇到 IO 等待 立即將控制權(quán)切,待 IO 完成后,重新將執(zhí)行流切回原來協(xié)程切出的點

  • 協(xié)程并行協(xié)程依次執(zhí)行,同上一個邏輯

  • 協(xié)程嵌套執(zhí)行流程由外向內(nèi)逐層進入,直到發(fā)生 IO,然后切到外層協(xié)程,父協(xié)程不會等待子協(xié)程結(jié)束

協(xié)程的執(zhí)行順序

先來看看基礎(chǔ)的例子:

go(function () {     echo "hello go1 n";});echo "hello main n";go(function () {     echo "hello go2 n";});

go() 是 Co::create() 的縮寫, 用來創(chuàng)建一個協(xié)程, 接受 callback 作為參數(shù), callback 中的代碼, 會在這個新建的協(xié)程中執(zhí)行.

備注: SwooleCoroutine 可以簡寫為 Co

上面的代碼執(zhí)行結(jié)果:

root@b98940b00a9b /v/w/c/p/swoole# php co.phphello go1 hello main hello go2

執(zhí)行結(jié)果和我們平時寫代碼的順序, 好像沒啥區(qū)別. 實際執(zhí)行過程:

  • 運行此段代碼, 系統(tǒng)啟動一個新進程

  • 遇到 go(), 當前進程中生成一個協(xié)程, 協(xié)程中輸出 heelo go1, 協(xié)程退出

  • 進程繼續(xù)向下執(zhí)行代碼, 輸出 hello main

  • 再生成一個協(xié)程, 協(xié)程中輸出heelo go2, 協(xié)程退出

運行此段代碼, 系統(tǒng)啟動一個新進程. 如果不理解這句話, 你可以使用如下代碼:

// co.php<?phpsleep(100);

執(zhí)行并使用 ps aux 查看系統(tǒng)中的進程:

root@b98940b00a9b /v/w/c/p/swoole# php co.php &? root@b98940b00a9b /v/w/c/p/swoole# ps auxPID   USER     TIME   COMMAND     1 root       0:00 php -a   10 root       0:00 sh   19 root       0:01 fish  749 root       0:00 php co.php  760 root       0:00 ps aux ?

我們來稍微改一改, 體驗協(xié)程的調(diào)度:

use Co;go(function () {     Co::sleep(1); // 只新增了一行代碼     echo "hello go1 n";});echo "hello main n";go(function () {     echo "hello go2 n";});

Co::sleep() 函數(shù)功能和 sleep() 差不多, 但是它模擬的是 IO等待(IO后面會細講). 執(zhí)行的結(jié)果如下:

root@b98940b00a9b /v/w/c/p/swoole# php co.phphello main hello go2 hello go1

怎么不是順序執(zhí)行的呢? 實際執(zhí)行過程:

  • 運行此段代碼, 系統(tǒng)啟動一個新進程
  • 遇到 go(), 當前進程中生成一個協(xié)程
  • 協(xié)程中遇到 IO阻塞 (這里是 Co::sleep() 模擬出的 IO等待), 協(xié)程讓出控制, 進入?yún)f(xié)程調(diào)度隊列
  • 進程繼續(xù)向下執(zhí)行, 輸出 hello main
  • 執(zhí)行下一個協(xié)程, 輸出 hello go2
  • 之前的協(xié)程準備就緒, 繼續(xù)執(zhí)行, 輸出 hello go1

到這里, 已經(jīng)可以看到 swoole 中 協(xié)程與進程的關(guān)系, 以及 協(xié)程的調(diào)度, 我們再改一改剛才的程序:

go(function () {     Co::sleep(1);     echo "hello go1 n";});echo "hello main n";go(function () {     Co::sleep(1);     echo "hello go2 n";});

我想你已經(jīng)知道輸出是什么樣子了:

root@b98940b00a9b /v/w/c/p/swoole# php co.phphello main hello go1 hello go2 ?

協(xié)程快在哪? 減少IO阻塞導致的性能損失

大家可能聽到使用協(xié)程的最多的理由, 可能就是 協(xié)程快. 那看起來和平時寫得差不多的代碼, 為什么就要快一些呢? 一個常見的理由是, 可以創(chuàng)建很多個協(xié)程來執(zhí)行任務(wù), 所以快. 這種說法是對的, 不過還停留在表面.

首先, 一般的計算機任務(wù)分為 2 種:

  • CPU密集型, 比如加減乘除等科學計算
  • IO 密集型, 比如網(wǎng)絡(luò)請求, 文件讀寫等

其次, 高性能相關(guān)的 2 個概念:

  • 并行: 同一個時刻, 同一個 CPU 只能執(zhí)行同一個任務(wù), 要同時執(zhí)行多個任務(wù), 就需要有多個 CPU 才行
  • 并發(fā): 由于 CPU 切換任務(wù)非常快, 快到人類可以感知的極限, 就會有很多任務(wù) 同時執(zhí)行 的錯覺

了解了這些, 我們再來看協(xié)程, 協(xié)程適合的是 IO 密集型 應(yīng)用, 因為協(xié)程在 IO阻塞 時會自動調(diào)度, 減少IO阻塞導致的時間損失.

我們可以對比下面三段代碼:

  • 普通版: 執(zhí)行 4 個任務(wù)
$n = 4;for ($i = 0; $i < $n; $i++) {     sleep(1);     echo microtime(true) . ": hello $i n";};echo "hello main n";
root@b98940b00a9b /v/w/c/p/swoole# time php co.php1528965075.4608: hello 01528965076.461: hello 11528965077.4613: hello 21528965078.4616: hello 3hello main real    0m 4.02s user    0m 0.01s sys     0m 0.00s ?
  • 單個協(xié)程版:
$n = 4;go(function () use ($n) {     for ($i = 0; $i < $n; $i++) {         Co::sleep(1);         echo microtime(true) . ": hello $i n";     };});echo "hello main n";
root@b98940b00a9b /v/w/c/p/swoole# time php co.phphello main1528965150.4834: hello 01528965151.4846: hello 11528965152.4859: hello 21528965153.4872: hello 3real    0m 4.03s user    0m 0.00s sys     0m 0.02s ?
  • 多協(xié)程版: 見證奇跡的時刻
$n = 4;for ($i = 0; $i < $n; $i++) {     go(function () use ($i) {         Co::sleep(1);         echo microtime(true) . ": hello $i n";     });};echo "hello main n";
root@b98940b00a9b /v/w/c/p/swoole# time php co.phphello main1528965245.5491: hello 01528965245.5498: hello 31528965245.5502: hello 21528965245.5506: hello 1real    0m 1.02s user    0m 0.01s sys     0m 0.00s ?

為什么時間有這么大的差異呢:

  • 普通寫法, 會遇到 IO阻塞 導致的性能損失

  • 單協(xié)程: 盡管 IO阻塞 引發(fā)了協(xié)程調(diào)度, 但當前只有一個協(xié)程, 調(diào)度之后還是執(zhí)行當前協(xié)程

  • 多協(xié)程: 真正發(fā)揮出了協(xié)程的優(yōu)勢, 遇到 IO阻塞 時發(fā)生調(diào)度, IO就緒時恢復(fù)運行

我們將多協(xié)程版稍微修改一下:

  • 多協(xié)程版2: CPU密集型
$n = 4;for ($i = 0; $i < $n; $i++) {     go(function () use ($i) {         // Co::sleep(1);         sleep(1);         echo microtime(true) . ": hello $i n";     });};echo "hello main n";
root@b98940b00a9b /v/w/c/p/swoole# time php co.php1528965743.4327: hello 01528965744.4331: hello 11528965745.4337: hello 21528965746.4342: hello 3hello main real    0m 4.02s user    0m 0.01s sys     0m 0.00s ?

只是將 Co::sleep() 改成了 sleep(), 時間又和普通版差不多了. 因為:

  • sleep() 可以看做是 CPU密集型任務(wù), 不會引起協(xié)程的調(diào)度

  • Co::sleep() 模擬的是 IO密集型任務(wù), 會引發(fā)協(xié)程的調(diào)度
    這也是為什么, 協(xié)程適合 IO密集型 的應(yīng)用.

再來一組對比的例子: 使用 redis

// 同步版, redis使用時會有 IO 阻塞$cnt = 2000;for ($i = 0; $i < $cnt; $i++) {     $redis = new Redis();     $redis->connect('redis');     $redis->auth('123');     $key = $redis->get('key');}// 單協(xié)程版: 只有一個協(xié)程, 并沒有使用到協(xié)程調(diào)度減少 IO 阻塞go(function () use ($cnt) {     for ($i = 0; $i < $cnt; $i++) {         $redis = new CoRedis();         $redis->connect('redis', 6379);         $redis->auth('123');         $redis->get('key');     }});// 多協(xié)程版, 真正使用到協(xié)程調(diào)度帶來的 IO 阻塞時的調(diào)度for ($i = 0; $i < $cnt; $i++) {     go(function () {         $redis = new CoRedis();         $redis->connect('redis', 6379);         $redis->auth('123');         $redis->get('key');     });}

性能對比:

# 多協(xié)程版root@0124f915c976 /v/w/c/p/swoole# time php co.phpreal    0m 0.54s user    0m 0.04s sys     0m 0.23s ?# 同步版root@0124f915c976 /v/w/c/p/swoole# time php co.phpreal    0m 1.48s user    0m 0.17s sys     0m 0.57s ?

swoole 協(xié)程和 go 協(xié)程對比: 單進程 vs 多線程

接觸過 go 協(xié)程的 coder, 初始接觸 swoole 的協(xié)程會有點 懵, 比如對比下面的代碼:

package main  import (     "fmt"     "time")func main() {     go func() {         fmt.Println("hello go")     }()      fmt.Println("hello main")      time.Sleep(time.Second)}
> 14:11 src $ go run test.go hello main hello go

剛寫 go 協(xié)程的 coder, 在寫這個代碼的時候會被告知不要忘了 time.Sleep(time.Second), 否則看不到輸出 hello go, 其次, hello go與 hello main 的順序也和 swoole 中的協(xié)程不一樣.

原因就在于 swoole 和 go 中, 實現(xiàn)協(xié)程調(diào)度的模型不同.

上面 go 代碼的執(zhí)行過程:

  • 運行 go 代碼, 系統(tǒng)啟動一個新進程
  • 查找 package main, 然后執(zhí)行其中的 func mian()
  • 遇到協(xié)程, 交給協(xié)程調(diào)度器執(zhí)行
  • 繼續(xù)向下執(zhí)行, 輸出 hello main
  • 如果不添加 time.Sleep(time.Second), main 函數(shù)執(zhí)行完, 程序結(jié)束, 進程退出, 導致調(diào)度中的協(xié)程也終止

go 中的協(xié)程, 使用的 MPG 模型:

  • M 指的是 Machine, 一個M直接關(guān)聯(lián)了一個內(nèi)核線程
  • P 指的是 processor, 代表了M所需的上下文環(huán)境, 也是處理用戶級代碼邏輯的處理器
  • G 指的是 Goroutine, 其實本質(zhì)上也是一種輕量級的線程

當被swoole協(xié)程三連問時,快哭了!

而 swoole 中的協(xié)程調(diào)度使用 單進程模型, 所有協(xié)程都是在當前進程中進行調(diào)度, 單進程的好處也很明顯 – 簡單 / 不用加鎖 / 性能也高.

無論是 go 的 MPG模型, 還是 swoole 的 單進程模型, 都是對 CSP理論 的實現(xiàn).

? 版權(quán)聲明
THE END
喜歡就支持一下吧
點贊13 分享