天天看點

Go語言goroutine和通道學習

Go語言裡的并發指的是能讓某個函數獨立于其他函數運作的能力。當一個函數建立為goroutine時,Go會将其視為一個獨立的工作機關。這個單元會被排程到可用的邏輯處理器上執行。

Go語言運作時的排程器是一個複雜的軟體,能管理被建立的所有goroutine并為其配置設定執行時間。這個排程器在作業系統之上,将作業系統的線程與語言運作時的邏輯處理器綁定,并在邏輯處理器上運作goroutine。排程器在任何給定的時間,都會全面控制那個goroutine要在那個邏輯處理器上運作。

一、協程

執行體是個抽象的概念,在作業系統層面層面有多個概念與之對應,比如作業系統自己掌管的程序、程序内的線程以及程序内的協程。與傳統的系統級别的線程與程序相比,協程的最大優勢在于其”輕量級”,可以輕松建立百萬個而不會導緻資源枯竭,遠遠高于線程程序數量。

Go語言在語言級别支援輕量級線程,叫goroutine。Go語言标準庫提供的所有系統調用操作,都會讓出CPU給其他goroutine,輕量級線程的切換管理不依賴系統的線程和程序,也不依賴CPU的核心數量。

如果建立一個goroutine并準備運作,這個goroutine會被放到排程器的全局運作隊列中。之後,排程器就将這些隊列中的goroutine配置設定給一個邏輯處理器,并放到這個邏輯處理器對應的本地運作隊列中。本地運作隊列中的goroutine會一直等待知道自己被配置設定的邏輯處理器執行。

Go語言運作時會把goroutine排程到邏輯處理器上運作。這個邏輯處理器綁定到唯一的作業系統線程。當goroutine可以運作的時候,會被放入邏輯處理器的執行隊列中。
Go語言goroutine和通道學習
當goroutine執行了一個阻塞的系統調用時,排程器會被這個線程與處理器分離,并建立一個新的線程來運作這個處理器提供的服務。
Go語言goroutine和通道學習

二、并發與并行

并行是讓不同的代碼片段同時在不同的實體處理器上執行。并行的關鍵是同時做很多事情,二并發是指同時管理很多事情,這些事情可能隻做了一般被暫停去做别的事情。很多情況下,并發的效果比并行好,因為作業系統和硬體的總資源一般很少,但能支援系統同時做很多事情。

如果希望goroutine并行,必須使用多于一個邏輯處理器。當有多個邏輯處理器時,排程器會将goroutine平等配置設定到每個邏輯處理器上。這會讓goroutine在不同的線程上運作。要想實作并行的效果,使用者需要讓自己的程式運作在多個實體處理器的機器上。

四、goroutine

func main(){
    runtime.GOMAXPROCS()

    var wg sync.WaitGroup
    wg.Add()

    fmt.Println("start goroutes")

    go func() {
        defer wg.Done()

        for count := ; count < ; count++ {
            for char := 'a'; char < 'a'+; char++ {
                fmt.Printf("%c", char)
            }
        }
    }()

    go func() {
        defer wg.Done()

        for count := ; count < ; count++ {
            for char := 'A'; char < 'A'+; char++ {
                fmt.Printf("%c", char)
            }
        }
    }()

    wg.Wait()

    fmt.Println("\nProgram exit")
}
           

程式的輸出結構為:

start goroutes
ABCDEABCDEABCDEabcdeabcdeabcde //可能先全小寫字母再大寫字母
Program exit
           
因為隻有一個邏輯處理器,而且每個goroutine處理花費的時間不長,所有每次先處理完一個goroutine再處理另外一個。

基于排程器的内部算法,一個正在運作的goroutine在結束之前,可以被停止并重新排程。排程器這樣做的目的是防止某個goroutine占用時間過長,排程器會停止目前運作的goroutine,并給其他可運作的goroutine運作的機會。

在使用goroutine程式設計的時候可能會遇到goroutine還沒開始執行主程式已經退出的情況,是以使用WaitGroup來進行同步,保證goroutine先執行完成在主程式退出。WaitGroup是一個計數信号量,可以用來記錄并維護運作的goroutine。如果WaitGroup的值大于0,Wait方法就會阻塞。關鍵字defer會修改函數的調用時機,在正在執行的函數傳回時才能真正調用defer聲明的函數。

使用WaitGroup時,建議在goroutine外使用WaitGroup.Add,以免Add未執行,Wait已經退出。

程式中調用了runtime包的GOMAXPROCS函數。這個函數允許程式更改排程器可以使用的邏輯處理器的數量。函數中傳參1,是通知排程器隻能為該程式用一個邏輯處理器。可以根據計算機的CPU個數指定邏輯處理器的個數runtime.GOMAXPROCES(runtime.NumCPU)。

五、通道

Go語言天然支援高并發很重要的原因是可以支援goroutine與通道類型,通過通道,發送和接受需要的資源,在goroutine之間做同步。

當一個資源需要在goroutine之間共享時,通道在goroutine之間架起了一個管道,并提供了確定同步交換資料的機制,聲明通道時,需要指定被共享資源的資料類型。可以通過通道共享内置類型、命名類型、結構類型和引用類型的值和指針。

在Go語言中需要使用内置函數make來建立一個通道:

strchan1:=make(chan string)
strchan2:=make(chan string)
           

使用内置函數make建立了兩個通道,一個無緩沖區的通道,一個有緩沖區的通道,make的第一個參數需要關鍵字chan,後面跟着允許通道交換的資料的類型。如果建立一個有緩沖區的通道,之後需要在第二個參數指定這個緩沖區的大小。

//放入一個字元串到通道
strchan2<-"test"
//從通道接受一個字元串
value:=<-strchan2
           

無緩沖區的通道要求發送goroutine和接受goroutine同時準備好,才能完成發送和接受操作。如果兩個goroutine沒有同時準備好,通道會導緻先執行發送或接受的goroutine阻塞等待。這種通道的發送和接受的互動行為本身就是同步的。

有緩沖區的通道是一種在被接受前能存儲一個或者多個值的通道。這種類型的通道并不會強制要求goroutine之間必須同時完成發送和接受。通道會阻塞發送和接受動作的條件也會不同。隻有在通道中沒有要接受的值時,接受動作才會阻塞。隻有通道沒有可緩沖去容納被發送的值時,發送動作才會被阻塞。無緩沖區的通道保證發送和接受的goroutine會在同一時間進行資料交換,有緩沖區沒有這個保證。

部分人可能認為無緩沖區就是緩沖區為1,其實有很大的差別。

c1:=make(chan int)        無緩沖
 c2:=make(chan int)      有緩沖
c1                            
           

無緩沖的 不僅僅是 向 c1 通道放 1 而是 一直要有别的攜程 <-c1 接手了 這個參數,那麼c1<-1才會繼續下去,要不然就一直阻塞着

而 c2<-1 則不會阻塞,因為緩沖大小是1 隻有當 放第二個值的時候 第一個還沒被人拿走,這時候才會阻塞。

由于通道變量本身就是指針,可用相等操作符判斷是否為同意對象或nil。

fun main(){
    var a,b chan int = make(chan int),make(chan int)

    var c chan bool

    fmt.Println(a==b)
    fmt.Println(c==nil)
}

false true
           

内置函數cap和len傳回緩沖區大小和目前緩沖區數量;而對于無緩沖區的通道都傳回為0,可以據此判斷通道是同步還是異步。

繼續閱讀