swoole 協程與go 協程對比
date: 2018-5-30 14:31:38
title: swoole| swoole 協程初體驗
description: 通過協程的執行初窺 swoole 中協程的排程; 理解協程為什麼快; swoole 協程和 go 協程對比
折騰 swoole 協程有一段時間了, 總結一篇入門貼, 希望對新手有幫助.
內容概覽:
- 協程的執行順序: 初窺 swoole 中協程的排程
- 協程為什麼快: 減少IO阻塞帶來的效能損耗
- swoole 協程和 go 協程對比: 單程序 vs 多執行緒
協程的執行順序
先來看看基礎的例子:
go(function () { echo "hello go1 \n"; }); echo "hello main \n"; go(function () { echo "hello go2 \n"; });
go()
是 \Co::create()
的縮寫, 用來建立一個協程, 接受 callback 作為引數, callback 中的程式碼, 會在這個新建的協程中執行.
備註: \Swoole\Coroutine
可以簡寫為 \Co
上面的程式碼執行結果:
[email protected] /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
檢視系統中的程序:
[email protected] /v/w/c/p/swoole# php co.php & ⏎ [email protected] /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後面會細講). 執行的結果如下:
[email protected] /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";
});
我想你已經知道輸出是什麼樣子了:
[email protected] /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";
[email protected] /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";
[email protected] /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";
[email protected] /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";
[email protected] /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');
});
}
效能對比:
# 多協程版
[email protected] /v/w/c/p/swoole# time php co.php
real 0m 0.54s
user 0m 0.04s
sys 0m 0.23s
⏎
# 同步版
[email protected] /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 模型
而 swoole 中的協程排程使用 單程序模型, 所有協程都是在當前程序中進行排程, 單程序的好處也很明顯 -- 簡單 / 不用加鎖 / 效能也高.
無論是 go 的 MPG模型, 還是 swoole 的 單程序模型, 都是對 CSP理論 的實現.
CSP通訊方式, 在1985年時的論文就已經有了, 做理論研究的人, 如果沒有能提前幾年, 十幾年甚至幾十年的大膽假設, 可能很難提高了.
寫在最後
今天從 go()
出發, 得以一瞥協程世界, 協程的世界裡還有很多很有意思的東西, 需要我們去發現. 比如:
- 我們普通版的程式碼是當前程序裡執行的, 只是單個程序, 可我們現在可能有了很多協程, 會不會有什麼奇遇呢?
還有一個細節: swoole 中有 Co::sleep()
和 sleep()
2個方法的, 而 go 中只有 time.Sleep()
一個方法?
這是 swoole 協程需要經歷的一個階段(畢竟 go 快 10 年了), 還不夠 智慧的判斷 IO阻塞, 所以上面也使用了相應的協程版 redis co\Redis()
-- 你得使用配套協程版, 才能達到協程排程的效果.
如果對協程的發展階段感興趣, 可以閱讀下面這篇文章:
- Why c++ coroutine?Why libgo?: 關於協程全景式的概述的, 推薦花時間讀一讀
想解鎖 swoole 協程的更多姿勢:
- Swoole 2.1 正式版釋出,協程+通道帶來全新的 PHP 程式設計模式
- Swoole 4.0 正式版,面向生產環境的 PHP 協程引擎
- Swoole4-全新的PHP程式設計模式_韓天峰_PHPCON2018
參考文獻: