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核數的最大值,並返回之前的值。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章