協程的執行順序:
go(function () { echo "hello go1 \n"; }); echo "hello main \n"; go(function () { echo "hello go2 \n"; });
go() 是 \Co::create() 的縮寫,用來創建一個協程,接受callback作為參數,callback中的代碼。會在這個新建的協程中執行。
備注:\Swoole\Coroutine 可以簡寫為 \Co
上面的代碼執行結果:
# php co.php hello go1 hello main hello go2
實際執行過程:
- 運行此段代碼,系統啟動一個新進程
- 遇到 go() ,當前進程中生成一個協程,協程中輸出 hello go1,協程退出
- 進程繼續向下執行代碼,輸出 hello main
- 再生成一個協程,協程中輸出 hello go2,協程退出
下面稍微改一下執行順序
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等待,執行的順序如下:
# php co.php hello main hello go2 hello go1
上面的實際執行過程如下:
- 運行此段代碼,系統啟動一個進程
- 遇到 go(),當前進程中生成一個協程
- 協程中遇到IO阻塞(這里是 Co::sleep() 模擬出來的IO等待),協程讓出控制,進入協程調度隊列
- 進程繼續向下執行,輸出 hello main
- 執行下一個協程,輸出 hello go2
- 之前的協程准備就緒,繼續執行,輸出 hello go1
協程快在哪?減少IO阻塞導致的性能損失
一般的計算機任務分為兩種:
- CPU密集型,比如加減乘除等科學計算
- IO密集型,比如網絡請求,文件讀寫等
高性能相關的兩個概念:
- 並行:同一個時刻,同一個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";
執行結果:
# 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";
執行結果:
# 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";
執行結果:
# 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就緒時恢復運行
下面將多協程版本修改為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";
執行的結果:
# 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'); }); }
性能對比:
# 多協程版 # php co.php real 0m 0.54s user 0m 0.04s sys 0m 0.23s # 同步版 # php co.php real 0m 1.48s user 0m 0.17s sys 0m 0.57s
swoole協程和go協程對比:單進程 VS 多線程
package main import ( "fmt" "time" ) func main() { go func() { fmt.Println("hello go") }() fmt.Println("hello main") time.Sleep(time.Second) }
執行結果:
$ go run test.go hello main hello go
go代碼的執行過程如下:
- 運行 go 代碼,系統啟動一個新進程
- 查找 package main ,然后執行其中的 func main()
- 遇到協程,交給協程調度器執行
- 繼續向下執行,輸出 hello main
- 如果不添加 time.Sleep(time.Second),main函數執行完,程序結束,進程退出,導致調度中的協程也終止
swoole和go實現協程調度的模型不同,go中使用的是MPG模型:
- M 指的是 Machine, 一個M直接關聯了一個內核線程
- P 指的是 processor, 代表了M所需的上下文環境, 也是處理用戶級代碼邏輯的處理器
- G 指的是 Goroutine, 其實本質上也是一種輕量級的線程
而swoole中的協程調度使用單進程模型,所有協程都是在當前進程中進行調度,單進程的好處是:簡單 / 不用加鎖 / 性能高。