Files
build-web-application-with-…/zh-tw/02.7.md
2019-02-26 01:40:54 +08:00

8.1 KiB
Raw Blame History

2.7 併發

有人把Go比作21世紀的C語言第一是因為Go語言設計簡單第二21世紀最重要的就是並行程式設計而Go從語言層面就支援了並行。

goroutine

goroutine是Go並行設計的核心。goroutine說到底其實就是協程但是它比執行緒更小十幾個goroutine可能體現在底層就是五六個執行緒Go語言內部幫你實現了這些goroutine之間的記憶體共享。執行goroutine只需極少的棧記憶體(大概是4~5KB)當然會根據相應的資料伸縮。也正因為如此可同時執行成千上萬個併發任務。goroutine比thread更易用、更高效、更輕便。

goroutine是透過Go的runtime管理的一個執行緒管理器。goroutine透過go關鍵字實現了,其實就是一個普通的函式。


go hello(a, b, c)

透過關鍵字go就啟動了一個goroutine。我們來看一個例子


package main

import (
	"fmt"
	"runtime"
)

func say(s string) {
	for i := 0; i < 5; i++ {
		runtime.Gosched()
		fmt.Println(s)
	}
}

func main() {
	go say("world") //開一個新的Goroutines執行
	say("hello") //當前Goroutines執行
}

// 以上程式執行後將輸出:
// hello
// world
// hello
// world
// hello
// world
// hello
// world
// hello

我們可以看到go關鍵字很方便的就實現了併發程式設計。 上面的多個goroutine執行在同一個程序裡面共享記憶體資料不過設計上我們要遵循不要透過共享來通訊而要透過通訊來共享。

runtime.Gosched()表示讓CPU把時間片讓給別人,下次某個時候繼續恢復執行該goroutine。

預設情況下在Go 1.5將標識併發系統執行緒個數的runtime.GOMAXPROCS的初始值由1改為了執行環境的CPU核數。

但在Go 1.5以前排程器僅使用單執行緒,也就是說只實現了併發。想要發揮多核處理器的並行,需要在我們的程式中顯式呼叫 runtime.GOMAXPROCS(n) 告訴排程器同時使用多個執行緒。GOMAXPROCS 設定了同時執行邏輯程式碼的系統執行緒的最大數量並返回之前的設定。如果n < 1不會改變當前設定。

channels

goroutine執行在相同的地址空間因此訪問共享記憶體必須做好同步。那麼goroutine之間如何進行資料的通訊呢Go提供了一個很好的通訊機制channel。channel可以與Unix shell 中的雙向管道做類別比可以透過它傳送或者接收值。這些值只能是特定的型別channel型別。定義一個channel時也需要定義傳送到channel的值的型別。注意必須使用make 建立channel


ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})

channel透過運算子<-來接收和傳送資料


ch <- v    // 傳送v到channel ch.
v := <-ch  // 從ch中接收資料並賦值給v

我們把這些應用到我們的例子中來:


package main

import "fmt"

func sum(a []int, c chan int) {
	total := 0
	for _, v := range a {
		total += v
	}
	c <- total  // send total to c
}

func main() {
	a := []int{7, 2, 8, -9, 4, 0}

	c := make(chan int)
	go sum(a[:len(a)/2], c)
	go sum(a[len(a)/2:], c)
	x, y := <-c, <-c  // receive from c

	fmt.Println(x, y, x + y)
}

預設情況下channel接收和傳送資料都是阻塞的除非另一端已經準備好這樣就使得Goroutines同步變的更加的簡單而不需要顯式的lock。所謂阻塞也就是如果讀取value := <-ch它將會被阻塞直到有資料接收。其次任何傳送ch<-5將會被阻塞直到資料被讀出。無緩衝channel是在多個goroutine之間同步很棒的工具。

Buffered Channels

上面我們介紹了預設的非快取型別的channel不過Go也允許指定channel的緩衝大小很簡單就是channel可以儲存多少元素。ch:= make(chan bool, 4)建立了可以儲存4個元素的bool 型channel。在這個channel 中前4個元素可以無阻塞的寫入。當寫入第5個元素時程式碼將會阻塞直到其他goroutine從channel 中讀取一些元素,騰出空間。


ch := make(chan type, value)

當 value = 0 時channel 是無緩衝阻塞讀寫的當value > 0 時channel 有緩衝、是非阻塞的,直到寫滿 value 個元素才阻塞寫入。

我們看一下下面這個例子你可以在自己本機測試一下修改相應的value值


package main

import "fmt"

func main() {
	c := make(chan int, 2)//修改2為1就報錯修改2為3可以正常執行
	c <- 1
	c <- 2
	fmt.Println(<-c)
	fmt.Println(<-c)
}
        //修改為1報如下的錯誤:
        //fatal error: all goroutines are asleep - deadlock!

Range和Close

上面這個例子中我們需要讀取兩次c這樣不是很方便Go考慮到了這一點所以也可以透過range像操作slice或者map一樣操作快取型別的channel請看下面的例子


package main

import (
	"fmt"
)

func fibonacci(n int, c chan int) {
	x, y := 1, 1
	for i := 0; i < n; i++ {
		c <- x
		x, y = y, x + y
	}
	close(c)
}

func main() {
	c := make(chan int, 10)
	go fibonacci(cap(c), c)
	for i := range c {
		fmt.Println(i)
	}
}

for i := range c能夠不斷的讀取channel裡面的資料直到該channel被顯式的關閉。上面程式碼我們看到可以顯式的關閉channel生產者透過內建函式close關閉channel。關閉channel之後就無法再發送任何資料了在消費方可以透過語法v, ok := <-ch測試channel是否被關閉。如果ok返回false那麼說明channel已經沒有任何資料並且已經被關閉。

記住應該在生產者的地方關閉channel而不是消費的地方去關閉它這樣容易引起panic

另外記住一點的就是channel不像檔案之類別的不需要經常去關閉只有當你確實沒有任何傳送資料了或者你想顯式的結束range迴圈之類別的

Select

我們上面介紹的都是隻有一個channel的情況那麼如果存在多個channel的時候我們該如何操作呢Go裡面提供了一個關鍵字select,透過select可以監聽channel上的資料流動。

select預設是阻塞的只有當監聽的channel中有傳送或接收可以進行時才會執行當多個channel都準備好的時候select是隨機的選擇一個執行的。


package main

import "fmt"

func fibonacci(c, quit chan int) {
	x, y := 1, 1
	for {
		select {
		case c <- x:
			x, y = y, x + y
		case <-quit:
			fmt.Println("quit")
			return
		}
	}
}

func main() {
	c := make(chan int)
	quit := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)
		}
		quit <- 0
	}()
	fibonacci(c, quit)
}

select裡面還有default語法select其實就是類似switch的功能default就是當監聽的channel都沒有準備好的時候預設執行的select不再阻塞等待channel


select {
case i := <-c:
	// use i
default:
	// 當c阻塞的時候執行這裡
}

超時

有時候會出現goroutine阻塞的情況那麼我們如何避免整個程式進入阻塞的情況呢我們可以利用select來設定超時透過如下的方式實現


func main() {
	c := make(chan int)
	o := make(chan bool)
	go func() {
		for {
			select {
				case v := <- c:
					println(v)
				case <- time.After(5 * time.Second):
					println("timeout")
					o <- true
					break
			}
		}
	}()
	<- o
}

runtime goroutine

runtime套件中有幾個處理goroutine的函式

  • Goexit

    退出當前執行的goroutine但是defer函式還會繼續呼叫

  • Gosched

    讓出當前goroutine的執行許可權排程器安排其他等待的任務執行並在下次某個時候從該位置恢復執行。

  • NumCPU

    返回 CPU 核數量

  • NumGoroutine

    返回正在執行和排隊的任務總數

  • GOMAXPROCS

    用來設定可以平行計算的CPU核數的最大值並返回之前的值。