本篇文章將介紹 hello world 的併發實現,其中涉及到的知識有:併發
在介紹 hello world 的程序實現前,先簡要介紹兩點: 1. 併發與並行的區別, 2: Go 的 GPM 調度系統函數
package main import ( "fmt" "runtime" "sync" ) var wg sync.WaitGroup func say_hello(value interface{}) { defer wg.Done() fmt.Printf("%v", value) } func common_say_hello() { wg.Add(5) go say_hello("w") go say_hello("o") go say_hello("r") go say_hello("l") go say_hello("d") } func main() { runtime.GOMAXPROCS(1) common_say_hello() wg.Wait() }
代碼介紹:操作系統
代碼運行結果以下:線程
dworl
爲何 d 會打印在最前面而 worl 則依次打印呢?<<Go 語言實戰>> 給出的解釋是「第一個 goroutine 完成全部顯示須要花的時間很短,以致於調度器切換到第二個 goroutine以前就完成了全部任務」。那麼,這裏的第一個 goroutine 是 「go say_hello("d")」 嗎?第二個,第三個 goroutine.. 又是哪一個呢?調度器根據怎麼的順序來調度 goroutine 的呢?這些問題留給咱們後續解答,有知道的朋友還請不吝賜教,感謝。code
上面的代碼限定了邏輯處理器的數量爲 1,因此這裏其實實現的是併發而沒有並行。當設置邏輯處理器的數量大於 1 時,即實現了並行也實現了併發。更改邏輯處理器數量爲 3,查看程序運行狀況:隊列
dorlw dowlr ldorw
執行了三次每次打印的輸出都不同。資源
那麼是否是到這裏就結束了呢?沒有。有一點須要說明的是: 一個正在運行的 goroutine 能夠被中止並從新調度。若是 goroutine 長時間佔用邏輯處理器,調度器會中止該 goroutine,並給其它 goroutine 運行的機會。
基於上述分析,更改 hello world 代碼,使得每一個 goroutine 佔用較長的邏輯處理器時間,查看 goroutine 是否被調度器切換,代碼以下:string
func multi_hello(prefix string) { defer wg.Done() next: for outer := 2; outer < 5000; outer++ { for inter := 2; inter < outer; inter++ { if outer%inter == 0 { continue next } } fmt.Println("say %s: %d times", prefix, outer) } } func crazy_say_hello() { wg.Add(5) go multi_hello("w") go multi_hello("o") go multi_hello("r") go multi_hello("l") go multi_hello("d") } func main() { runtime.GOMAXPROCS(1) crazy_say_hello() wg.Wait() }
查看代碼運行結果:it
say r: 4327 times say r: 4337 times say r: 4339 times say w: 4493 times say w: 4507 times say w: 4513 times ... say w: 4999 times say r: 4349 times say r: 4357 times ...
這裏僅截取部分執行結果。能夠看到,第 94-95 行調度器切換 「r goroutine」 到 「w goroutine」 ,而後在 99-100 行又從 「w goroutine」 切換到 「r goroutine」。import
上述 hello world 的 goroutine 均不涉及對公共資源的訪問,所以它們能和諧共存,互不干擾。若是涉及到公共資源的訪問,goroutine 將變得至關「野蠻」也即出現相互競爭訪問公共資源的狀態,這種狀況稱爲「競爭」狀態。
進一步的改寫 hello world 程序以下:
var helloTimes int32 func cal_hello_num(prefix string) { defer wg.Done() value := helloTimes runtime.Gosched() value++ helloTimes = value fmt.Printf("say %s: %d times\n", prefix, helloTimes) } func num_say_hello() { wg.Add(5) go cal_hello_num("w") go cal_hello_num("o") go cal_hello_num("r") go cal_hello_num("l") go cal_hello_num("d") } func main() { runtime.GOMAXPROCS(1) num_say_hello() wg.Wait() }
爲方便說明這裏將邏輯處理器的數量設爲 1,同時引入 runtime 包的 Gosched 函數,該函數會將當前 goroutine 從線程退出,並放回到邏輯處理器的隊列中。
程序執行結果以下:
say d: 1 times say w: 1 times say o: 1 times say r: 1 times say l: 1 times
屢次執行每一個 goroutine 打印結果均爲 1,爲何呢?
分析上述代碼,每一個 goroutine 都會覆蓋另外一個 goroutine 的工做(競爭狀態所以存在)。每一個 goroutine 均創造了變量 helloTimes 的副本 value,當 goroutine 切換時每一個 goroutine 會將本身維護的 value 賦值給 helloTimes,致使 helloTimes 的值一直是 1。
那麼,若是每一個 goroutine 都不創造變量的副本是否這種競爭狀態就消失了呢?
進一步改寫程序以下:
改寫版本1
func cal_hello_num(prefix string) { defer wg.Done() helloTimes++ runtime.Gosched() fmt.Printf("say %s: %d times\n", prefix, helloTimes) } // 運行結果 say d: 5 times say w: 5 times say o: 5 times say r: 5 times say l: 5 times
改寫版本 2
func cal_hello_num(prefix string) { defer wg.Done() runtime.Gosched() helloTimes++ fmt.Printf("say %s: %d times\n", prefix, helloTimes) } // 運行結果 say d: 1 times say w: 2 times say o: 3 times say r: 4 times say l: 5 times
版本 1 和版本 2 移動了 helloTimes++ 相對於 GoSched 的位置,卻獲得了徹底不一樣的結果。其實不難理解,由於 helloTimes 是全局變量,每一個 goroutine 都維護這個變量。因此,在版本一中每一個 goroutine 切換以前都會對全局變量 helloTimes 加 1,加 1 完成後,程序依次打印「最終值」 5。而版本二 goroutine 在切換以後對全局變量加 1,其效果至關於每一個 goroutine 按順序依次執行全局變量的自增操做。
多個 goroutine 訪問共享資源極易出現「幺蛾子」,在程序中能夠經過鎖住共享資源的方式來避免競爭狀態的出現。
能夠經過原子函數,互斥鎖鎖住共享資源,實現 goroutine 對共享資源的順序訪問。
未完待續..