Goroutine
Goroutine
算是 Go 語言最大的亮點了。在 Go 裡面,每一個併發執行的活動的都稱為 Goroutine。當開始接觸 Goroutine 的時候,可以將它類比為傳統的執行緒,但你絕不能把它當作執行緒,兩者是有本質的區別的。
當你開始寫 Go 程式的時候,必須有一個主函式 main
,這個是由主 Goroutine 來呼叫。新的 goroutine 由關鍵字 go
來建立。並且 go 關鍵字支援匿名函式。像這樣:
go func() {
// 執行
fmt.Println("this is a goroutine")
}()
就是這麼簡單,很輕鬆的建立一個 Goroutine,這樣就可以開啟你的併發之旅了。
Go 的併發模型
還記得上面講了的嗎?Goroutine 和執行緒是有本質區別的。那麼 Goroutine 到底是什麼呢?Goroutine 既不是 OS 執行緒,也不是綠色執行緒(使用者空間的執行緒,有執行的語言管理)。它們是更加抽象且輕量級的,稱之為協程。協程是非搶佔式的且不能被中斷的 Goroutine (函式,閉包或者方法),但是協程有多個點,可以暫停或者重新進入。
Goroutine
是由一個名為 M:N 的排程器實現的,他是將 M 個綠色執行緒對映到 N 個 OS 執行緒,然後再將 Goroutine 安排到綠色執行緒。當 Gouroutine 的數量超過了可用的綠色執行緒的時候,排程程式將處理分佈線上程上的 Gotoutine,並且確保這些 Goroutine 被阻塞的時候,其他的 Goroutine 可以執行。這就是 Goroutine 的併發模型。
Fork-Join 模型
Go 語言還遵循這個一個稱為 Fork-Join
的模型。fork
指的是在程式中的任意一點,他可以將執行的子分支與自己的父節點同時執行。join
指的是在將來的某個時候,這些分支會合並在一起。下面的示意圖:
下面開看一個簡單的例子:
func main() {
go func() {
fmt.Println("hello")
}()
// do something
fmt.Println("world")
}
本例中,使用了一個 Goroutine 開啟了一個協程執行,主函式則執行其餘部分。在這個例子中,其實是沒有 Join 點的,因為你不知道這個協程將在哪個時間點退出,這是不確定的。單單就這個例子而言,我們是不確定是否會列印出 “hello world” 的。Goroutine 雖然被建立了,並且在執行時執行,但是實際上很可能在 main goroutine 退出之後執行,你將看不到任何輸出。
ps:如果你看到了也不必有任何疑問,因為這是不確定的。
你可以增加一個競爭條件,例如在 main Goroutine 加入 time.Sleep
,但這並不是建立 Join 點,因為你還是並不能保證 goroutine 一定執行,你還是不確定退出時間。Join 點是保證程式的正確性和消除競爭條件的關鍵。
所以想要建立一個 Join 點,你必須同步 main Goroutine 和 子 goroutine。這可以通過多種方式來實現,比如 Sync
包。下一篇就主要介紹一下這個包。
下面來看一個有意思的現象,關於 Join 點的問題。注意這裡將會使用 Sync
包。來看一下下面的兩段程式碼:
無 Join 點
str := "hello"
go func() {
defer wg.Done()
str = "world"
}()
fmt.Println(str)
有 Join 點
var wg sync.WaitGroup
str := "hello"
wg.Add(1)
go func() {
defer wg.Done()
str = "world"
}()
wg.Wait() // 這裡就是 join 點
fmt.Println(str)
當你執行第一段的程式碼的時候,結果在大多數可能下會是 hello
。當然並不能說是一定,因為這是不確定的。除非你加入競爭條件(其實也是不確定的,只不過加大了概率)或者像第二段程式碼一樣加入了 Join
點。使用 Sync
包之後,引入 Join 點的情況下,可以很確定結果是 world
。因為他保證了程式的正確性,以第二段程式碼為列的話,可以認為我們確定了 str
變數確確實實被重新賦予了新的值。這是一個確定的事件,所以結果也是確定的。
從上面的例子我們還可以看出來一點,就是 goroutine 在他們所建立的相同的地址空間執行(main goroutine 和 子 goroutine 都同時使用了 str),再來看一段程式碼:
var wg sync.WaitGroup
str := "hello"
for _, str = range []string{"H", "J", "K"} {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(str)
}()
}
wg.Wait()
請思考一下結果是什麼?H, J, K
或者是 H, K, J
?? 很多人可能會認為結果會是無序列印三個值。可是結果並不是你想像的那樣,結果大多數情況下會是列印三個 K
。雖然上面已經說了 goroutine 執行在相同的地址空間,但是為什麼並不會無序的列印出三個值呢?來看一下這整個過程,goroutine 內列印的是 str
的字串引用,也就是說整個迭代過程中 str
一直是在變的,當我們建立 goroutine 的時候,因為不確定 goroutine 的在什麼時間執行,很可能當 goroutine 執行的時候迭代已經結束了。所以這個例子的程式碼大概率將會看到三個 k
的輸出。可以加大陣列的長度加以驗證 goroutine 的執行的時間是不確定性的證明。那麼這裡還有一個問題 。
既然迭代結束了, str 的會不會被回收呢?當然從結果來看,GO 語言還是保留了 str 的引用。
那麼該如何解決這個問題呢?既然他每次只能引用到迭代處的值。那麼就只需要把迭代的值傳入就可以了。
var wg sync.WaitGroup
str := "hello"
for _, str = range []string{"H", "J", "K"} {
wg.Add(1)
go func(s string) {
defer wg.Done()
fmt.Println(s)
}(str)
}
wg.Wait()
閱讀中有任何建議和疑問歡迎反饋,有不恰當的地方歡迎修正。共同進步