hello world 的併發實現


本篇文章將介紹 hello world 的併發實現,其中涉及到的知識有:併發

  • 併發與並行
  • GPM

在介紹 hello world 的程序實現前,先簡要介紹兩點: 1. 併發與並行的區別, 2: Go 的 GPM 調度系統函數

hello world 的併發實現

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

代碼介紹:操作系統

  • runtime 包的 GOMAXPROCS 函數容許程序更改調度器可使用的邏輯處理器數量,邏輯處理器和操做系統線程是一一綁定的關係。這裏僅使用 1 個邏輯處理器處理併發運行的 goroutine。
  • 實現 goroutine 很簡單隻須要在函數名前加 go 便可讓該函數獨立於其它函數運行,Go 會將其視爲一個獨立的工做單元,這個單元會被調度到可用的邏輯處理器上執行。
  • 使用了 sync 包中結構體 WaitGroup 的 Add/Wait/Done 方法來等待 goroutine 的完成。若是不加等待, main 函數會在 goroutine 運行前終止。

代碼運行結果以下:線程

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 將變得至關「野蠻」也即出現相互競爭訪問公共資源的狀態,這種狀況稱爲「競爭」狀態。

競爭狀態的 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 對共享資源的順序訪問。

原子函數

未完待續..

相關文章
相關標籤/搜索