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() }

小結

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