什么是進程?
進程就是應用程序的啟動實例。獨立的文件資源,數據資源,內存空間。
什么是線程?
線程屬于進程,是程序的執行者。一個進程至少包含一個主線程,也可以有更多的子線程。線程有兩種調度策略,一是:分時調度,二是:搶占式調度。
什么是協程?
協程是輕量級線程,協程也是屬于線程,協程是在線程里執行的。協程的調度是用戶手動切換的,所以又叫用戶空間線程。協程的創建、切換、掛起、銷毀全部為內存操作,消耗是非常低的。協程的調度策略是:協作式調度。
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, 其實本質上也是一種輕量級的線程

而 swoole 中的協程調度使用 單進程模型, 所有協程都是在當前進程中進行調度, 單進程的好處也很明顯 -- 簡單 / 不用加鎖 / 性能也高.
無論是 go 的 MPG模型, 還是 swoole 的 單進程模型, 都是對 CSP理論 的實現.
以上就是“面試官的swoole協程三連問”的詳細內容,感興趣的小伙伴歡迎關注編程學習網,每日干貨分享
掃碼二維碼 獲取免費視頻學習資料
- 本文固定鏈接: http://www.wangchenghua.com/post/8050/
- 轉載請注明:轉載必須在正文中標注并保留原文鏈接
- 掃碼: 掃上方二維碼獲取免費視頻資料