golang goroutine協程執行機制及使用詳解
Go(又稱Golang )是Google 開發的一種靜態 強型別 、編譯型、併發型,並具有垃圾回收功能的程式語言 。Go於2009年正式推出,國內各大網際網路公司都有使用,尤其是七牛雲,基本都是golang寫的,
傳聞Go是為併發而生的語言,執行速度僅比c c++慢一點,內建協程(輕量級的執行緒),說白了協程還是執行在一個執行緒上,由排程器來排程執行緒該執行哪個協程,也就是類似於模擬了一個作業系統排程執行緒,我們也知道, 其實多執行緒說白了也是輪流佔用cpu,其實還是順序執行的 ,協程也是一樣,他也是輪流獲取執行機會,只不過他獲取的是執行緒, 但是如果cpu是多核的話,多執行緒就能真正意義上的實現併發 同時,如果GO執行過程中有多個執行緒的話,協程也能實現真正意義上的併發執行,所以, 最理想的情況,根據cpu核數開闢對應數量的執行緒,通過這些執行緒,來為協程提供執行環境
當我們在開發網路應用程式時,遇到的瓶頸總是在io上,由此出現了多程序,多執行緒,非同步io的解決方案,其中非同步io最為優秀,因為他們在不佔用過多的資源情況下完成高效能io操作,但是非同步io會導致一個問題,那就是回撥地獄,node js之前深受詬病的地方就在於此,後來出現了async await這種方案,真正的實現了同步式的寫非同步,其實Go的協程也是這樣,有人把goroutine叫做纖程,認為node js的async await才是真正的協程,對此我不做評價,關於goroutine的執行機制本文不講,大家可以看這篇 博文,講的很生動,本文主要對goroutine的使用進行講解,如果大家熟悉node js的async await或者c#的async(其實node js就是學習的c#的async await),可以來對比一下兩者在使用上的不同,從而對協程纖程的概念產生進一步的瞭解
在golang中開闢一個協程非常簡單,只需要一個go關鍵字
package main import ( "fmt" "time" ) func main(){ for i := 0;i<10;i++{ go func(i int){ for{ fmt.Printf("%d",i); } }(i) } time.Sleep(time.Millisecond); }
列印結果
可以看到,完全是隨機的,列印哪個取決於排程器對協程的排程,
goroutine相比於執行緒,有個特點,那就是非搶佔式,如果一個協程佔據了執行緒,不主動釋放或者沒有發生阻塞的話,那麼永遠不會交出執行緒的控制權,我們舉個例子來驗證下
package main import ( "time" ) func main(){ for i := 0;i<10;i++{ go func(i int){ for{ i++ } }(i) } time.Sleep(time.Millisecond); }
這段程式在執行後,永遠不會退出,並且佔滿了cpu,原因就是goroutine中,一直在執行i++,沒有釋放,而一直佔用執行緒,當四個執行緒佔滿之後,其他的所有goroutine都沒有執行的機會了,所以本該一秒鐘後就退出的程式一直沒有退出,cpu滿載再跑,但是為什麼前面例子的Printf沒有發生這種情況呢?是因為Printf其實是個io操作,io操作會阻塞,阻塞的時候goroutine就會自動的釋放出對執行緒的佔有,所以其他的goroutine才有執行的機會,除了io阻塞,golang還提供了一個api,讓我們可以手動交出控制權,那就是Gosched(),當我們呼叫這個方法時,goroutine就會主動釋放出對執行緒的控制權
package main import ( "time" "runtime" ) func main(){ for i := 0;i<10;i++{ go func(i int){ for{ i++; runtime.Gosched(); } }(i) } time.Sleep(time.Millisecond); }
修改之後,一秒鐘之後,程式碼正常退出
常見的觸發goroutine切換,有一下幾種情況
1、I/O,select 2、channel 3、等待鎖 4、函式呼叫(是一個切換的機會,是否會切換由排程器決定) 5、runtime.Gosched()
說完了goroutine的基本用法,接下來我們說一下goroutine之間的通訊,Go中通訊的理念是“不要通過共享資料來通訊,而是通過通訊來共享資料“,Go中實現通訊主要通過channel,它類似於unix shell中的雙向管道,可以接受和傳送資料,
我們來看個例子,
package main import( "fmt" "time" ) func main(){ c := make(chan int) go func(){ for{ n := <-c; fmt.Printf("%d",n) } }() c <- 1; c <- 2; time.Sleep(time.Millisecond); }
列印結果為12
,我們通過make來建立channel型別,並指明存放的資料型別,通過<-
來接收和傳送資料,c <- 1
為向channel c傳送資料1,n := <-c;
表示從channel c接收資料,預設情況下,傳送資料和接收資料都是阻塞的,這很容易讓我們寫出同步的程式碼,因為阻塞,所以會很容易發生goroutine的切換,並且,資料被髮送後一定要被接收,不然會一直阻塞下去,程式會報錯退出,
本例中,首先向c傳送資料1,main goroutine阻塞,執行開闢的協程,從而讀到資料,列印資料,然後main協程阻塞完成,向c傳送第二個資料2,開闢的協程還在阻塞讀取資料,成功讀取到資料2時,列印2,一秒鐘後,主函式退出,所有goroutine銷燬,程式退出
我們仔細看這份程式碼,其實有個問題,在開闢的goroutine中,我們一直再迴圈阻塞的讀取c中的資料,並不知道c什麼時候寫入完成,不再寫入,如果c不再寫入我們完全可以銷燬這個goroutine,不必佔有資源,通過close api我們可以完成這一任務,
package main import ( "fmt" "time" ) func main(){ c := make(chan int); go func(){ for{ p,ok := <-c; if(!ok){ fmt.Printf("jieshu"); return } fmt.Printf("%d",p); } }() for i := 0;i<10;i++{ c<-i } close(c); }
當我們對channel寫入完成後,可以呼叫close
方法來顯式的告訴接收方對channel的寫入已經完畢,這是,在接收的時候我們可以根據接收的第二個值,一個boolean值來判斷是否完成寫入,如果為false的話,表示此channel已經關閉,我們沒有必要繼續對channel進行阻塞的讀,
除了判斷第二個boolean引數,go還提供了range來對channel進行迴圈讀取,當channel被關閉時就會退出迴圈,
package main import ( "fmt" "time" ) func main(){ c := make(chan int); go func(){ //for{ //p,ok := <-c; //if(!ok){ //fmt.Printf("jieshu"); //return //} for p := range c{ fmt.Printf("%d",p); } fmt.Printf("jieshu"); //} }() for i := 0;i<10;i++{ c<-i } close(c); time.Sleep(time.Millisecond); }
兩種方式列印的都是123456789jieshu
另外,通過Buffered Channels我們可以建立帶快取的channel,使用方法為建立channel時傳入第二個引數,指明快取的數量,
package main import "fmt" func main() { c := make(chan int, 2)//修改2為1就報錯,修改2為3可以正常執行 c <- 1 c <- 2 fmt.Println(<-c) fmt.Println(<-c) }
例子中,我們建立channel時,傳入引數2,便可以儲存兩個兩個資料,前兩個資料的寫入可以無阻塞的,不需要等待資料被讀出,如果我們連續寫入三個資料,就會報錯,阻塞在第三個資料的寫入出無法進行下一步
最後,我們說一下select,這個和作業系統io模型中的select很像,先執行先到達的channel我們看個例子
package main import ( "fmt" "time" ) func main(){ c := make(chan int); c2:= make(chan int); go func(){ for{ select{ case p := <- c : fmt.Printf("c:%d\n",p); case p2:= <- c2: fmt.Printf("c2:%d\n",p2); } } }() for i :=0;i<10;i++{ go func(i int){ c <- i }(i) go func (i int){ c2 <-i }(i) } time.Sleep(5*time.Millisecond); }
列印結果為
c:0 c2:1 c:1 c:2 c2:0 c:3 c:4 c:5 c:7 c2:2 c:6 c:8 c:9 c2:3 c2:5 c2:4 c2:6 c2:7 c2:8 c2:9
可以看到,c和c2的接收完全是隨機的,誰先接收到執行誰的回撥,當然這不僅限於接收,傳送資料時也可以使用select函式,另外,和switch語句一樣,golang中的select函式也支援設定default,當沒有接收到值的時候就會執行default回撥,如果沒有設定default,就會阻塞在select函式處,直到某一個傳送或者接收完成。
golang中 goroutine的基本使用就是這些,大家可以根據上面goroutine執行機制的文章和本文一起來體會golang的執行過程。
補充一個runtime包的幾個處理函式
-
Goexit
退出當前執行的goroutine,但是defer函式還會繼續呼叫 -
Gosched
讓出當前goroutine的執行許可權,排程器安排其他等待的任務執行,並在下次某個時候從該位置恢復執行。 -
NumCPU
返回 CPU 核數量 -
NumGoroutine
返回正在執行和排隊的任務總數 -
GOMAXPROCS
用來設定可以平行計算的CPU核數的最大值,並返回之前的值。