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); } 

打印結果

5551600088800499999991117777777742222220000044444444888888888999 9666665111177777777777777777777777777333333333333333399999999999 999999999999999999999999999999444442224444444488888888222222222 20888886666666655555555555444011111111111111000000000999999555555 5554444444000077777666666311111197777778888222277777753333444444 9999997777772222000077774444444444444444444 

可以看到,完全是隨機的,打印哪個取決於調度器對協程的調度,
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核數的最大值,並返回之前的值。


from:https://www.jianshu.com/p/68dbeb6d0112


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM