Go 語言進階——併發程式設計| 青訓營筆記
theme: cyanosis highlight: github
這是我參與「第五屆青訓營」伴學筆記創作活動的第 4 天
前言
本文從併發程式設計的視角來了解Go高效能的本質、為何Go的執行可以如此之快,主要涉及到知識點:併發、並行、協程、CSP模型
、Channel
、Mutex
、WaitGroup
等。
併發程式設計
併發VS並行
併發
是指多個執行緒在同一個CPU
上執行,主要是通過時間片的切換看起來像多個程式在同時執行,只不過CPU時間片的切換十分迅速,我們感知不到。
並行
是指多個執行緒在多個CPU
上執行,多個執行緒在同一時間同時執行而不是時間片的切換。
Go
可以充分發揮多核CPU的優勢,高效執行。
協程
在Go
實現高併發的機制裡,還有一個重要的概念——協程Goroutine
,協程也叫做輕量級執行緒
。
執行緒
在建立時需要消耗一定的系統資源,執行緒屬於核心態,執行緒的建立、切換、停止都屬於比較重量級的系統操作,佔用系統棧資源屬於MB
級別。
協程
是屬於使用者態的,屬於輕量級執行緒,協程的建立、切換、銷燬都是由Go
語言本身完成,比執行緒消耗的資源少的多,佔用系統棧資源屬於KB
級別。
一個執行緒裡可以同時執行多個協程,Go
可以同時建立上萬級別的協程,也是Go
支援高併發原因之一。
程式碼示例
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
提倡通過通訊共享內容而不是通過共享記憶體而實現通訊。
通道
通過通訊共享記憶體涉及到另一個概念——通道Channel
。
Go
可以使用Channel
控制子協程,在建立協程時需要建立同樣數量的Channel
。
每個通道只允許交換指定型別的資料,在Go
中使用chan
關鍵字來宣告一個通道,使用 close
函式來關閉通道。
通過操作符<-
來指定通道的方向,實現傳送或接收。
通道的建立
make(chan 元素型別, [緩衝大小])
通道分類: 1. 無緩衝通道 make(chan int) 2. 有緩衝通道 make(chan int, 2)
無緩衝通道也被稱為同步通道。
有緩衝通道也是一個生產-消費模型。
程式碼示例
下面通過一個程式碼示例,看一下通道的具體使用,示例通過協程和通道完成輸出一個數的平方功能。
程式碼功能邏輯如下:
- A子協程傳送0 ~ 9數字
- B子協程計算輸入數字的平方
- 主協程輸出最後的平方數
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
是一個計數訊號量,WaitGroup
和 Java
中的 CyclicBarrier
、CountDownLatch
非常類似,可以用來記錄並維護執行的 goroutine
。如果 WaitGroup
的值大於 0
,Wait
方法就會阻塞,常用來實現併發程式設計時的同步操作。
WaitGroup
有3
個方法,Add
、Done
、Wait
。
使用方法:當開啟協程時呼叫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模型
、Channel
、Mutex
、WaitGroup
等。