相信很多讀者和我一樣,也都是衝着 Go 大肆宣揚的高併發而入坑,從源碼的角度來看,協程goroutine本質是由谷歌實現的超級“線程池”。每個goroutine大約 4-5KB的棧內存消耗的確更加符合目前web服務的場景,和由於實現機制和輕量級的開銷,Go的高併發性的確有目共睹。
goroutine
在我們使用JAVA或者C編寫網絡程序時,基本都是用一個線程來處理一個http請求, 但是這樣的方式對於資源的利用率不高。而Go語言就實現了這樣一種輕量級線程的機制,GO語言在底層封裝了所有的系統調用,自己實現了一個調度器,這種設計在操作系統的代碼中非常多見,比如現代的操作系統基本都會封裝一個軟件的Timer,同時可以提供上萬個軟Timer同時工作,而這只是基於數量很少的硬件timer實現的,而GO語言中的併發也是如此,他是基於線程的調度池,這種調度的單元在Go語言中被稱爲goroutine。
Go 程序中使用 go 關鍵字爲一個函數創建一個 goroutine。goroutine 必定對應一個函數。爲一個普通函數創建 goroutine 的方式如下:
go 函數名( 參數列表 )
當然也可以使用匿名函數來創建goroutine方式如下:
go func( 參數列表 ){
函數體
}( 調用參數列表 )
注意使用匿名來啓動goroutine是可以和閉包結合的
下面我們來舉例說明,在下面的例程當中我們先通過running函數來創建一個goroutine1,並使用匿名函數來創造了另一個goroutine2,並且在最後使用sleep功能阻塞主函數,給兩個子goroutine以運行的機會。
package main
import (
"fmt"
"time"
)
func running() {
var times int
for {// 構建一個無限循環
times++
fmt.Println("this is runing function's tick", times)//從筆者的機器上看runing函數中的ticks累加到300左右
}
}
func main() {
go running() //通過普通函數調用goroutine
go func() { //通過匿名函數調用goroutine
var times int //注意這個times變量其實和匿名函數共同構成閉包
for {
times++
fmt.Println("this is anonymous function's tick ", times)//從筆者的機器上看runing函數中的ticks累加到300左右
}
}()
time.Sleep(time.Millisecond)//在本例中使用休眠的方式來阻塞main函數創建的主goroutine
}
1.例程的執行時序
剛剛的例程中程序運行的時序圖如下,這個例子中,G程序在啓動時會創建一個main() 函數的goroutine也稱主goroutine。在 main() 函數執行到 go running 語句時,歸屬於 running() 函數的 goroutine 被創建,執行到go func 語句時,歸屬於匿名函數的 goroutine 被創建,這兩個goroutine實際都是由主goroutine創建的,在主goroutine未放棄CPU的情況下,子goroutine不會被調度執行,直到執行sleep(time.Millisecond)主goroutine被阻塞,處於就緒態的子goroutine纔有執行機會,而當sleep到時後,主 goroutine退出運行態,此時兩個子Goroutine也隨之退出。具體如下圖:
2.goroutine的調度策略
這裏再爲各位讀者補充一下必要的操作系統知識,與一般操作系統一樣,協程也將運行狀態抽象歸爲三種:
就緒(Ready):該goroutine在就緒列表中,只等待CPU。
運行(Running):該goroutine正在執行。
阻塞(Blocked):該goroutine不在就緒列表中。包含掛起、延時、等待信號量、等待事件等等。
下次我們再來探討一下GO語言中是不是隻有通過time.sleep才能讓子的goroutine得以執行。