Go 語言進階——併發程式設計| 青訓營筆記

語言: CN / TW / HK

theme: cyanosis highlight: github


這是我參與「第五屆青訓營」伴學筆記創作活動的第 4 天

前言

本文從併發程式設計的視角來了解Go高效能的本質、為何Go的執行可以如此之快,主要涉及到知識點:併發、並行、協程、CSP模型ChannelMutexWaitGroup等。

併發程式設計

併發VS並行

併發是指多個執行緒在同一個CPU上執行,主要是通過時間片的切換看起來像多個程式在同時執行,只不過CPU時間片的切換十分迅速,我們感知不到。

並行是指多個執行緒在多個CPU上執行,多個執行緒在同一時間同時執行而不是時間片的切換。

Go可以充分發揮多核CPU的優勢,高效執行。

image.png

協程

Go實現高併發的機制裡,還有一個重要的概念——協程Goroutine,協程也叫做輕量級執行緒

執行緒在建立時需要消耗一定的系統資源,執行緒屬於核心態,執行緒的建立、切換、停止都屬於比較重量級的系統操作,佔用系統棧資源屬於MB級別。

協程是屬於使用者態的,屬於輕量級執行緒,協程的建立、切換、銷燬都是由Go語言本身完成,比執行緒消耗的資源少的多,佔用系統棧資源屬於KB級別。

一個執行緒裡可以同時執行多個協程,Go可以同時建立上萬級別的協程,也是Go支援高併發原因之一。

image.png

程式碼示例

Go建立協程十分簡單,只需要在呼叫的函式前加一個go關鍵字即可。

```go import ( "fmt" "time" )

func hello(i int) { println("hello world : " + fmt.Sprint(i)) }

func main() { for i := 0; i < 5; i++ { // 開啟協程 go hello(i) } // 等協程執行結束後,主執行緒再結束 time.Sleep(time.Second) } ```

CSP模型

CSP(Communicating Sequential Process)通訊順序程序、交談循序程式,也被譯為交換訊息的循序程式,它是一種用來描述併發行系統之間進行互動的模型。

CSP最大的優點就是靈活,但也很容易出現死鎖。

Go提倡通過通訊共享內容而不是通過共享記憶體而實現通訊。

image.png

通道

通過通訊共享記憶體涉及到另一個概念——通道Channel

Go可以使用Channel控制子協程,在建立協程時需要建立同樣數量的Channel

每個通道只允許交換指定型別的資料,在Go中使用chan關鍵字來宣告一個通道,使用 close函式來關閉通道。

通過操作符<-來指定通道的方向,實現傳送或接收。

通道的建立

make(chan 元素型別, [緩衝大小])

通道分類: 1. 無緩衝通道 make(chan int) 2. 有緩衝通道 make(chan int, 2)

無緩衝通道也被稱為同步通道。

有緩衝通道也是一個生產-消費模型。

image.png

程式碼示例

下面通過一個程式碼示例,看一下通道的具體使用,示例通過協程和通道完成輸出一個數的平方功能。

程式碼功能邏輯如下:

  1. A子協程傳送0 ~ 9數字
  2. B子協程計算輸入數字的平方
  3. 主協程輸出最後的平方數

go func main() { src := make(chan int) dest := make(chan int, 3) // A子協程 go func() { defer close(src) for i := 0; i < 10; i++ { // 將數字傳送到channel src <- i } }() // B子協程 go func() { defer close(dest) for i := range src { // 將計算好的數字傳送到channel dest <- i * i } }() for i := range dest { println(i) } }

併發安全Lock

Go也有類似Java的鎖機制,這種機制是通過共享記憶體來實現通訊的。

Mutex

Go加鎖可以使用Mutex來實現,通過加鎖可以實現多個協程在同一時間只有獲取到鎖的協程來執行,其他協程只能等待鎖的釋放,Mutex是一種互斥鎖。

下面通過一個例子來看下多個協程對一個數字的相加,通過加鎖與不加鎖來看下兩者的區別。

go var ( x int64 lock sync.Mutex ) func addWithLock() { for i := 0; i < 2000; i++ { lock.Lock() x++ lock.Unlock() } } func addWithoutLock() { for i := 0; i < 2000; i++ { x++ } } func main() { x = 0 for i := 0; i < 5; i++ { go addWithoutLock() } time.Sleep(time.Second) println("withoutLock:", x) x = 0 for i := 0; i < 5; i++ { go addWithLock() } time.Sleep(time.Second) println("withLock:", x) } 執行程式碼輸出結果為:

withoutLock: 8113 withLock: 10000 其中withoutLock的結果每次執行結果可能不同,可以看出在不加鎖的情況下,得出的結果是不滿足預期結果的,也就是出現了記憶體不安全的資源競爭。

WaitGroup

Go中的 WaitGroup 是一個計數訊號量,WaitGroupJava 中的 CyclicBarrierCountDownLatch 非常類似,可以用來記錄並維護執行的 goroutine。如果 WaitGroup的值大於 0Wait 方法就會阻塞,常用來實現併發程式設計時的同步操作。

WaitGroup3個方法,AddDoneWait

使用方法:當開啟協程時呼叫Add方法增加一個計數,完成時呼叫Done方法減去一個計數,Wait會一直阻塞直到WaitGroup的值為 0

下面使用WaitGroup來實現一個等待所有協程執行完的例子:

go func main() { var wg sync.WaitGroup // 開啟5個計數 wg.Add(5) for i := 0; i < 5; i++ { go func(j int) { // 執行完時呼叫Donw defer wg.Done() hello(j) }(i) } // 等待所有協程執行完 wg.Wait() }

總結

本文主要涉及到知識點:併發、並行、協程、CSP模型ChannelMutexWaitGroup等。

引用

Go 語言進階與依賴管理