編程學習網 > PHP技術 > swoole > 面試官的swoole協程三連問
2021
06-17

面試官的swoole協程三連問

什么是進程?

進程就是應用程序的啟動實例。獨立的文件資源,數據資源,內存空間。

什么是線程?

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

什么是協程?

協程是輕量級線程,協程也是屬于線程,協程是在線程里執行的。協程的調度是用戶手動切換的,所以又叫用戶空間線程。協程的創建、切換、掛起、銷毀全部為內存操作,消耗是非常低的。協程的調度策略是:協作式調度。

swoole 協程的原理

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

  • Swoole server 接收數據在 worker 進程觸發 onReceive 回調,產生一個攜程。Swoole 為每個請求創建對應攜程。協程中也能創建子協程。

  • 協程在底層實現上是單線程的,因此同一時間只有一個協程在工作,協程的執行是串行的。

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

在這里插入圖片描述

Swoole 的協程執行流程

  • 協程沒有 IO 等待 正常執行 PHP 代碼,不會產生執行流程切換

  • 協程遇到 IO 等待 立即將控制權切,待 IO 完成后,重新將執行流切回原來協程切出的點

  • 協程并行協程依次執行,同上一個邏輯

  • 協程嵌套執行流程由外向內逐層進入,直到發生 IO,然后切到外層協程,父協程不會等待子協程結束

協程的執行順序

先來看看基礎的例子:

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

go()是\Co::create()的縮寫, 用來創建一個協程, 接受 callback 作為參數, callback 中的代碼, 會在這個新建的協程中執行.

備注:\Swoole\Coroutine可以簡寫為\Co

上面的代碼執行結果:

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

執行結果和我們平時寫代碼的順序, 好像沒啥區別. 實際執行過程:

  • 運行此段代碼, 系統啟動一個新進程

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

  • 進程繼續向下執行代碼, 輸出hello main

  • 再生成一個協程, 協程中輸出heelo go2, 協程退出

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

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

執行并使用ps aux查看系統中的進程:

root@b98940b00a9b /v/w/c/p/swoole# php co.php & ?
root@b98940b00a9b /v/w/c/p/swoole# ps aux PID 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
? 

我們來稍微改一改, 體驗協程的調度:

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

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

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

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

  • 運行此段代碼, 系統啟動一個新進程
  • 遇到go(), 當前進程中生成一個協程
  • 協程中遇到 IO阻塞 (這里是Co::sleep()模擬出的 IO等待), 協程讓出控制, 進入協程調度隊列
  • 進程繼續向下執行, 輸出hello main
  • 執行下一個協程, 輸出hello go2
  • 之前的協程準備就緒, 繼續執行, 輸出hello go1

到這里, 已經可以看到 swoole 中 協程與進程的關系, 以及 協程的調度, 我們再改一改剛才的程序:

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

我想你已經知道輸出是什么樣子了:

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

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

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

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

  • CPU密集型, 比如加減乘除等科學計算
  • IO 密集型, 比如網絡請求, 文件讀寫等

其次, 高性能相關的 2 個概念:

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

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

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

  • 普通版: 執行 4 個任務
$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.php 1528965075.4608: hello 0 1528965076.461: hello 1 1528965077.4613: hello 2 1528965078.4616: hello 3 hello main
real 0m 4.02s
user 0m 0.01s
sys 0m 0.00s
? 
  • 單個協程版:
$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.php hello main 1528965150.4834: hello 0 1528965151.4846: hello 1 1528965152.4859: hello 2 1528965153.4872: hello 3 real 0m 4.03s
user 0m 0.00s
sys 0m 0.02s
? 
  • 多協程版: 見證奇跡的時刻
$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.php hello main 1528965245.5491: hello 0 1528965245.5498: hello 3 1528965245.5502: hello 2 1528965245.5506: hello 1 real 0m 1.02s
user 0m 0.01s
sys 0m 0.00s
? 

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

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

  • 單協程: 盡管 IO阻塞 引發了協程調度, 但當前只有一個協程, 調度之后還是執行當前協程

  • 多協程: 真正發揮出了協程的優勢, 遇到 IO阻塞 時發生調度, IO就緒時恢復運行

我們將多協程版稍微修改一下:

  • 多協程版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.php 1528965743.4327: hello 0 1528965744.4331: hello 1 1528965745.4337: hello 2 1528965746.4342: hello 3 hello main
real 0m 4.02s
user 0m 0.01s
sys 0m 0.00s
? 

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

  • sleep()可以看做是 CPU密集型任務, 不會引起協程的調度

  • Co::sleep()模擬的是 IO密集型任務, 會引發協程的調度
    這也是為什么, 協程適合 IO密集型 的應用.

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

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

性能對比:

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

swoole 協程和 go 協程對比: 單進程 vs 多線程

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

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

原因就在于 swoole 和 go 中, 實現協程調度的模型不同.

上面 go 代碼的執行過程:

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

go 中的協程, 使用的 MPG 模型:

  • M 指的是 Machine, 一個M直接關聯了一個內核線程
  • P 指的是 processor, 代表了M所需的上下文環境, 也是處理用戶級代碼邏輯的處理器
  • G 指的是 Goroutine, 其實本質上也是一種輕量級的線程
MPG 模型
MPG 模型

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

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

以上就是“面試官的swoole協程三連問”的詳細內容,感興趣的小伙伴歡迎關注編程學習網,每日干貨分享

掃碼二維碼 獲取免費視頻學習資料

Python編程學習

查 看2022高級編程視頻教程免費獲取