swoole教程介紹相關(guān)協(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é)程執(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é)程調(diào)度使用 單進程模型, 所有協(xié)程都是在當前進程中進行調(diào)度, 單進程的好處也很明顯 – 簡單 / 不用加鎖 / 性能也高.
無論是 go 的 MPG模型, 還是 swoole 的 單進程模型, 都是對 CSP理論 的實現(xiàn).