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 語言進階與依賴管理