golang goroutine協(xié)程運行機制及使用詳解

Go(又稱Golang)是Google開發(fā)的一種靜態(tài)強類型、編譯型、并發(fā)型,并具有垃圾回收功能的編程語言。Go于2009年正式推出,國內(nèi)各大互聯(lián)網(wǎng)公司都有使用,尤其是七牛云,基本都是golang寫的,
傳聞Go是為并發(fā)而生的語言,運行速度僅比c c++慢一點,內(nèi)置協(xié)程(輕量級的線程),說白了協(xié)程還是運行在一個線程上,由調(diào)度器來調(diào)度線程該運行哪個協(xié)程,也就是類似于模擬了一個操作系統(tǒng)調(diào)度線程,我們也知道,其實多線程說白了也是輪流占用cpu,其實還是順序執(zhí)行的,協(xié)程也是一樣,他也是輪流獲取執(zhí)行機會,只不過他獲取的是線程,但是如果cpu是多核的話,多線程就能真正意義上的實現(xiàn)并發(fā)同時,如果GO執(zhí)行過程中有多個線程的話,協(xié)程也能實現(xiàn)真正意義上的并發(fā)執(zhí)行,所以,最理想的情況,根據(jù)cpu核數(shù)開辟對應(yīng)數(shù)量的線程,通過這些線程,來為協(xié)程提供執(zhí)行環(huán)境
當(dāng)我們在開發(fā)網(wǎng)絡(luò)應(yīng)用程序時,遇到的瓶頸總是在io上,由此出現(xiàn)了多進(jìn)程,多線程,異步io的解決方案,其中異步io最為優(yōu)秀,因為他們在不占用過多的資源情況下完成高性能io操作,但是異步io會導(dǎo)致一個問題,那就是回調(diào)地獄,node js之前深受詬病的地方就在于此,后來出現(xiàn)了async await這種方案,真正的實現(xiàn)了同步式的寫異步,其實Go的協(xié)程也是這樣,有人把goroutine叫做纖程,認(rèn)為node js的async await才是真正的協(xié)程,對此我不做評價,關(guān)于goroutine的運行機制本文不講,大家可以看這篇博文,講的很生動,本文主要對goroutine的使用進(jìn)行講解,如果大家熟悉node js的async await或者c#的async(其實node js就是學(xué)習(xí)的c#的async await),可以來對比一下兩者在使用上的不同,從而對協(xié)程纖程的概念產(chǎn)生進(jìn)一步的了解
在golang中開辟一個協(xié)程非常簡單,只需要一個go關(guān)鍵字

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

打印結(jié)果

5551600088800499999991117777777742222220000044444444888888888999
9666665111177777777777777777777777777333333333333333399999999999
999999999999999999999999999999444442224444444488888888222222222
20888886666666655555555555444011111111111111000000000999999555555
5554444444000077777666666311111197777778888222277777753333444444
9999997777772222000077774444444444444444444

可以看到,完全是隨機的,打印哪個取決于調(diào)度器對協(xié)程的調(diào)度,
goroutine相比于線程,有個特點,那就是非搶占式,如果一個協(xié)程占據(jù)了線程,不主動釋放或者沒有發(fā)生阻塞的話,那么永遠(yuǎn)不會交出線程的控制權(quán),我們舉個例子來驗證下

package main
  
import (
       "time"
)
func main(){
        for i := 0;i<10;i++{
                go func(i int){
                        for{
                                i++                                
                          }
                }(i)
        }
        time.Sleep(time.Millisecond);
}

這段程序在執(zhí)行后,永遠(yuǎn)不會退出,并且占滿了cpu,原因就是goroutine中,一直在執(zhí)行i++,沒有釋放,而一直占用線程,當(dāng)四個線程占滿之后,其他的所有g(shù)oroutine都沒有執(zhí)行的機會了,所以本該一秒鐘后就退出的程序一直沒有退出,cpu滿載再跑,但是為什么前面例子的Printf沒有發(fā)生這種情況呢?是因為Printf其實是個io操作,io操作會阻塞,阻塞的時候goroutine就會自動的釋放出對線程的占有,所以其他的goroutine才有執(zhí)行的機會,除了io阻塞,golang還提供了一個api,讓我們可以手動交出控制權(quán),那就是Gosched(),當(dāng)我們調(diào)用這個方法時,goroutine就會主動釋放出對線程的控制權(quán)

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

修改之后,一秒鐘之后,代碼正常退出
常見的觸發(fā)goroutine切換,有一下幾種情況

1、I/O,select

2、channel

3、等待鎖

4、函數(shù)調(diào)用(是一個切換的機會,是否會切換由調(diào)度器決定)

5、runtime.Gosched()

說完了goroutine的基本用法,接下來我們說一下goroutine之間的通信,Go中通信的理念是“不要通過共享數(shù)據(jù)來通信,而是通過通信來共享數(shù)據(jù)“,Go中實現(xiàn)通信主要通過channel,它類似于unix shell中的雙向管道,可以接受和發(fā)送數(shù)據(jù),
我們來看個例子,

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


}

打印結(jié)果為12,我們通過make來創(chuàng)建channel類型,并指明存放的數(shù)據(jù)類型,通過 <-來接收和發(fā)送數(shù)據(jù),c <- 1為向channel c發(fā)送數(shù)據(jù)1,n := <-c;表示從channel c接收數(shù)據(jù),默認(rèn)情況下,發(fā)送數(shù)據(jù)和接收數(shù)據(jù)都是阻塞的,這很容易讓我們寫出同步的代碼,因為阻塞,所以會很容易發(fā)生goroutine的切換,并且,數(shù)據(jù)被發(fā)送后一定要被接收,不然會一直阻塞下去,程序會報錯退出,
本例中,首先向c發(fā)送數(shù)據(jù)1,main goroutine阻塞,執(zhí)行開辟的協(xié)程,從而讀到數(shù)據(jù),打印數(shù)據(jù),然后main協(xié)程阻塞完成,向c發(fā)送第二個數(shù)據(jù)2,開辟的協(xié)程還在阻塞讀取數(shù)據(jù),成功讀取到數(shù)據(jù)2時,打印2,一秒鐘后,主函數(shù)退出,所有g(shù)oroutine銷毀,程序退出

我們仔細(xì)看這份代碼,其實有個問題,在開辟的goroutine中,我們一直再循環(huán)阻塞的讀取c中的數(shù)據(jù),并不知道c什么時候?qū)懭胪瓿?,不再寫入,如果c不再寫入我們完全可以銷毀這個goroutine,不必占有資源,通過close api我們可以完成這一任務(wù),

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

當(dāng)我們對channel寫入完成后,可以調(diào)用close方法來顯式的告訴接收方對channel的寫入已經(jīng)完畢,這是,在接收的時候我們可以根據(jù)接收的第二個值,一個boolean值來判斷是否完成寫入,如果為false的話,表示此channel已經(jīng)關(guān)閉,我們沒有必要繼續(xù)對channel進(jìn)行阻塞的讀,
除了判斷第二個boolean參數(shù),go還提供了range來對channel進(jìn)行循環(huán)讀取,當(dāng)channel被關(guān)閉時就會退出循環(huán),

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我們可以創(chuàng)建帶緩存的channel,使用方法為創(chuàng)建channel時傳入第二個參數(shù),指明緩存的數(shù)量,

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

例子中,我們創(chuàng)建channel時,傳入?yún)?shù)2,便可以存儲兩個兩個數(shù)據(jù),前兩個數(shù)據(jù)的寫入可以無阻塞的,不需要等待數(shù)據(jù)被讀出,如果我們連續(xù)寫入三個數(shù)據(jù),就會報錯,阻塞在第三個數(shù)據(jù)的寫入出無法進(jìn)行下一步

最后,我們說一下select,這個和操作系統(tǒng)io模型中的select很像,先執(zhí)行先到達(dá)的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);
}

打印結(jié)果為

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的接收完全是隨機的,誰先接收到執(zhí)行誰的回調(diào),當(dāng)然這不僅限于接收,發(fā)送數(shù)據(jù)時也可以使用select函數(shù),另外,和switch語句一樣,golang中的select函數(shù)也支持設(shè)置default,當(dāng)沒有接收到值的時候就會執(zhí)行default回調(diào),如果沒有設(shè)置default,就會阻塞在select函數(shù)處,直到某一個發(fā)送或者接收完成。

golang中 goroutine的基本使用就是這些,大家可以根據(jù)上面goroutine運行機制的文章和本文一起來體會golang的運行過程。

補充一個runtime包的幾個處理函數(shù)

  • Goexit
    退出當(dāng)前執(zhí)行的goroutine,但是defer函數(shù)還會繼續(xù)調(diào)用
  • Gosched
    讓出當(dāng)前goroutine的執(zhí)行權(quán)限,調(diào)度器安排其他等待的任務(wù)運行,并在下次某個時候從該位置恢復(fù)執(zhí)行。
  • NumCPU
    返回 CPU 核數(shù)量
  • NumGoroutine
    返回正在執(zhí)行和排隊的任務(wù)總數(shù)
  • GOMAXPROCS
    用來設(shè)置可以并行計算的CPU核數(shù)的最大值,并返回之前的值。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容