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

語言: CN / TW / HK

theme: nico

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

前言

記錄加入青訓營的每一天的日筆記

併發程式設計

併發與並行的區別

併發:多執行緒程式在一個核的CPU上執行

並行:多執行緒程式在多個核的CPU上執行

Go可以充分發揮多核優勢 高效執行

image.png

協程Goroutine

協程:使用者態,輕量級執行緒 棧MB級別

執行緒:核心態,執行緒跑多個協程,棧KB級別

image.png

執行緒的建立、切換、停止較大地佔用系統資源

協程的建立和排程由Go語言進行完成

通過開啟協程快速列印hello goroutine案例:

package concurrence ​ import ( "fmt" "time" ) ​ func hello(i int) { println("hello goroutine : " + fmt.Sprint(i)) } ​ func HelloGoRoutine() { for i := 0; i < 5; i++ {        // go關鍵字作為建立協程的關鍵字 go func(j int) { hello(j) }(i) }    // 保證子協程執行完前主執行緒不退出 time.Sleep(time.Second) }

CSP(communicating sequential processes)併發模型

不同於傳統的多執行緒通過共享記憶體來通訊,CSP講究的是“以通訊的方式來共享記憶體”。

Do not communicate by sharing memory; instead, share memory by communicating. “不要以共享記憶體的方式來通訊,相反,要通過通訊來共享記憶體。”

Channel 緩衝通道

建立方式:

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

通道是用來傳遞資料的一個資料結構,可以用於兩個goroutine之間,通過傳遞一個指定型別的值來同步執行和通訊。

操作符<-用於指定通道的方向,實現傳送or接收

若未指定方向,則為雙向通道

  • 無緩衝通道 make(chan int)
  • 有緩衝通道 make(chan int, 2)

image.png

通過兩個Channel通道完成數字平方任務案例:

package concurrence ​ func CalSquare() { src := make(chan int) dest := make(chan int, 3) go func() { defer close(src) for i := 0; i < 10; i++ { src <- i } }() go func() { defer close(dest) for i := range src { dest <- i * i } }() for i := range dest { //複雜操作 println(i) } }

注意:

  • 如果通道不帶緩衝,傳送方會阻塞直到接收方從通道中接收了值。如果通道帶緩衝,傳送方則會阻塞直到傳送的值被拷貝到緩衝區內;如果緩衝區已滿,則意味著需要等待直到某個接收方獲取到一個值。接收方在有值可以接收之前會一直阻塞。
  • 上述程式碼中之所以能夠順利從通道接收到資料,是因為每次遍歷之前都通過關閉對應的通道後再進行的遍歷接受資料

併發安全Lock

若採用共享記憶體實現通訊,則會出現多個Goroutine同時操作一塊記憶體資源的情況,這種情況會發生競態問題(資料競態)

Mutex互斥鎖解決資料競爭

互斥鎖是一種常用的控制共享資源訪問的方法,它能夠保證同時只有一個goroutine可以訪問共享資源。Go語言中使用sync包的Mutex型別來實現互斥鎖。

package concurrence ​ import ( "sync" "time" ) ​ var ( x    int64 lock sync.Mutex ) ​ func addWithLock() { for i := 0; i < 2000; i++ { lock.Lock() x += 1 lock.Unlock() } } func addWithoutLock() { for i := 0; i < 2000; i++ { x += 1 } } ​ func Add() { 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) } ​ func ManyGoWait() { var wg sync.WaitGroup wg.Add(5) for i := 0; i < 5; i++ { go func(j int) { defer wg.Done() hello(j) }(i) } wg.Wait() }

使用互斥鎖能夠保證同一時間有且只有一個goroutine進入臨界區,其他的goroutine則在等待鎖;

當互斥鎖釋放後,等待的goroutine才可以獲取鎖進入臨界區,多個goroutine同時等待一個鎖時,喚醒的策略是隨機的。

WaitGroup解決資料競爭

Go語言中除了可以使用通道(channel)和互斥鎖進行兩個併發程式間的同步外,還可以使用等待組進行多個任務的同步,等待組可以保證在併發環境中完成指定數量的任務 WaitGroup 值在內部維護著一個計數,此計數的初始預設值為零。

package concurrence ​ import ( "fmt" "sync" ) ​ func HelloPrint(i int) { fmt.Println("Hello WaitGroup :", i) } ​ func ManyGoWait() { var wg sync.WaitGroup wg.Add(5) for i := 0; i < 5; i++ { go func(j int) { defer wg.Done() HelloPrint(j) }(i) } wg.Wait() } ​ func main() { ManyGoWait() }

小結

今天學習到的內容還需要進一步的消化,我也是打算將併發程式設計這一塊的內容熟悉透徹了再進行下一部分的課程學習。如果筆記中有錯誤的地方也希望掘友們可以及時的提出糾正。