Go 並發編程原理
Go 語言的協程實現被稱之為 goroutine,由 Go 運行時管理,在 Go 語言中通過協程實現並發編程非常簡單:我們可以在一個處理進程中通過關鍵字 go
啟用多個協程,然后在不同的協程中完成不同的子任務,這些用戶在代碼中創建和維護的協程本質上是用戶級線程,Go 語言運行時會在底層通過調度器將用戶級線程交給操作系統的系統級線程去處理,如果在運行過程中遇到某個 IO 操作而暫停運行,調度器會將用戶級線程和系統級線程分離,以便讓系統級線程去處理其他用戶級線程,而當 IO 操作完成,需要恢復運行,調度器又會調度空閑的系統級線程來處理這個用戶級線程,從而達到並發處理多個協程的目的。此外,調度器還會在系統級線程不夠用時向操作系統申請創建新的系統級線程,而在系統級線程過多的情況下銷毀一些空閑的線程,這個過程和 PHP-FPM 的工作機制有點類似,實際上這也是很多進程/線程池管理器的工作機制,這樣一來,可以保證對系統資源的高效利用,避免系統資源的浪費。
以上,就是 Go 語言並發編程的獨特實現模型。
協程簡單示例
下面通過一個簡單的示例來演示如何在 Go 語言中通過協程進行並發編程,我們在 add.go
中編寫一個加法函數 add
並通過協程的方式來調用它:
package main import "fmt" func add(a, b int) { var c = a + b fmt.Printf("%d + %d = %d", a, b, c) } func main() { go add(1, 2) }
嗯,就是這么簡單,在這段代碼中包含了兩個協程,一個是顯式的,通過 go
關鍵字聲明的這條語句,表示啟用一個新的協程來處理加法運算,另一個是隱式的,即 main
函數本身也是運行在一個主協程中,該協程和調用 add
函數的子協程是並發運行的兩個協程,就好比從 go
關鍵字開始,從主協程中叉出一條新路。
並發執行示例
目前為止,我們僅僅演示了 Go 語言協程的啟動和簡單使用,但是通過上述代碼還不足以驗證協程是並發執行的,接下來,我們通過下面這段代碼來驗證協程的並發執行
package main import ( "fmt" "time" ) func add(a, b int) { var c = a + b fmt.Printf("%d + %d = %d\n", a, b, c) } func main() { for i := 0; i < 10; i++ { go add(1, i) } time.Sleep(1e9) }
通過 channel 進行消息傳遞
前面我們說到通道是一種數據類型,和數組/切片類型類似,一個通道只能傳遞一種類型的值,這個類型需要在聲明 通道時指定。在使用通道時,需要通過 make
進行聲明,通道對應的類型關鍵字是 chan
:
ch := make(chan int)
這里我們初始化了一個通道類型 ch
,其中只能傳遞 int
類型的值。
我們可以把通道看作是一個先進先出(FIFO)的隊列,通道中的元素會嚴格按照發送順序排列,繼而按照排列順序被接收,通道元素的發送和接收都可以通過 <-
操作符來實現,發送時元素值在右,通道變量在左:
ch <- 1 // 表示把元素 1 發送到通道 ch
接收時通道變量在右,可以通過指定變量接收元素值:
element := <-ch
也可以留空表示忽略:
<-ch
package main import ( "fmt" "time" ) func add(a, b int, ch chan int) { c := a + b fmt.Printf("%d + %d = %d\n", a, b, c) ch <- 1 } func main() { start := time.Now() chs := make([]chan int, 10) for i := 0; i < 10; i++ { chs[i] = make(chan int) go add(1, i, chs[i]) } for _, ch := range chs { <- ch } end := time.Now() consume := end.Sub(start).Seconds() fmt.Println("程序執行耗時(s):", consume) }