Go的Waitgroup和鎖

鹿呦呦發表於2021-05-18

學 Go 的時候知道 Go 語言支援併發,最簡單的方法是通過 go 關鍵字開啟 goroutine 即可。可在工作中,用的是 sync 包的 WaitGroup,然而這樣還不夠,當多個 goroutine 同時訪問一個變數時,還要考慮如何保證這些 goroutine 之間不會相互影響,這就又使用到了 sync 的 Mutex。

一、Goroutinue

先說 goroutine,我們都知道它是 Go 中的輕量級執行緒。Go 程式從 main 包的 main() 函式開始,在程式啟動時,Go 程式就會為 main() 函式建立一個預設的 goroutine。使用 goroutine,使用關鍵字 go 即可。

package main
import (
    "fmt"
)
func main() {
    // 併發執行程式
    go running()
}
func running() {
    fmt.Println("Goroutine")
}

執行程式碼會發現沒有我們預期的“Goroutine”輸出,這是因為當前的程式是一個單執行緒的程式,main 函式只要執行後,就不會再管其他執行緒在做什麼事情,程式就自動退出了。解決辦法是加一個 sleep 函式,讓 main 函式等待 running 函式執行完畢後再退出。我們假設 running 函式裡的程式碼執行需要 2 秒,因此讓 main 函式等待 3 秒再退出。

package main
import (
    "fmt"
    "time"
)
func main() {
    // 併發執行程式
    go running()
    time.Sleep(3 * time.Second)
}
func running() {
    fmt.Println("Goroutine")
}

再次執行程式碼,終端輸出了我們想要的“Goroutine”字串。

二、WaitGroup

上面我們是假設了 running 函式執行需要 2 秒,可如果執行需要 10 秒甚至更長時間,不知道 goroutin 什麼時候結束,難道還要 main 函式 sleep 更多的秒數嗎?就不能讓 running 函式執行完去通知 main 函式,main 函式收到訊號自動退出嗎?還真可以!可以使用 sync 包的 Waitgroup 判斷一組任務是否完成。

WatiGroup 能夠一直等到所有的 goroutine 執行完成,並且阻塞主執行緒的執行,直到所有的 goroutine 執行完成。它有 3 個方法:

  • Add():給計數器新增等待 goroutine 的數量。
  • Done():減少 WaitGroup 計數器的值,應在協程的最後執行。
  • Wait():執行阻塞,直到所有的 WaitGroup 數量變成 0

一個簡單的示例如下:

package main 
import ( 
    "fmt” 
    "sync” 
    “time"
) 

func process(i int, wg *sync.WaitGroup) { 
    fmt.Println("started Goroutine ", i) 
    time.Sleep(2 * time.Second) 
    fmt.Printf("Goroutine %d ended\n", i) 
    wg.Done() 
} 

func main() { 
    var wg sync.WaitGroup 
    for i := 0; i < 3; i++ { 
        wg.Add(1) 
        go process(i, &wg) 
    } 
    wg.Wait() 
    fmt.Println("All go routines finished executing”) 
}
//main函式也可以寫成如下方式
func main() {
    var wg sync.WaitGroup
    wg.Add(3) //設定計數器,數值即為goroutine的個數
    go process(1, &wg)
    go process(2, &wg)
    go process(3, &wg)
    wg.Wait() //主goroutine阻塞等待計數器變為0
    fmt.Println("All goroutines finished executing")
}

命令列輸出如下:

deer@192 src % go run hello.go //第1次
started Goroutine  3
started Goroutine  1
started Goroutine  2
Goroutine 2 ended
Goroutine 1 ended
Goroutine 3 ended
All goroutines finished executing

deer@192 src % go run hello.go //第2次
started Goroutine  3
started Goroutine  1
started Goroutine  2
Goroutine 1 ended
Goroutine 2 ended
Goroutine 3 ended
All goroutines finished executing

deer@192 src % go run hello.go //第3次
started Goroutine  3
started Goroutine  2
started Goroutine  1
Goroutine 3 ended
Goroutine 1 ended
Goroutine 2 ended
All goroutines finished executing

簡單的說,上面程式中 wg 內部維護了一個計數器,啟用了 3 個 goroutine:
1)每次啟用 goroutine 之前,都先呼叫 Add() 方法增加一個需要等待的 goroutine 計數。
2)每個 goroutine 都執行 process() 函式,這個函式在執行完成時需要呼叫 Done() 方法來表示 goroutine 的結束。
3)啟用 3 個 goroutine 後,main 的 goroutine 會執行到 Wait(),由於每個啟用的 goroutine 執行的 process() 都需要睡眠 2 秒,所以 main 的 goroutine 在 Wait() 這裡會阻塞一段時間(大約2秒),
4)當所有 goroutine 都完成後,計數器減為 0,Wait() 將不再阻塞,於是 main 的 goroutine 得以執行後面的 Println()。

這裡需要注意:
1)process() 中使用指標型別的 *sync.WaitGroup 作為引數,表示這 3 個 goroutine 共享一個 wg,才能知道這 3 個 goroutine 都完成了。如果這裡使用值型別的 sync.WaitGroup 作為引數,意味著每個 goroutine 都拷貝一份 wg,每個 goroutine 都使用自己的 wg,main goroutine將會永久阻塞而導致產生死鎖。
2)Add() 設定的數量必須與實際等待的 goroutine 個數一致,也就是和Done的呼叫數量必須相等,否則會panic,報錯資訊如下:

fatal error: all goroutines are asleep - deadlock!

三、鎖

當多個 goroutine 同時操作一個變數時,會存在資料競爭,導致最後的結果與期待的不符,解決辦法就是加鎖。Go 中的 sync 包 實現了兩種鎖:Mutex 和 RWMutex,前者為互斥鎖,後者為讀寫鎖,基於 Mutex 實現。當我們的場景是寫操作為主時,可以使用 Mutex 來加鎖、解鎖。

var lock sync.Mutex //宣告一個互斥鎖
 lock.Lock() //加鎖
//code...
 lock.Unlock() //解鎖

互斥鎖其實就是每個執行緒在對資源操作前都嘗試先加鎖,成功加鎖才能操作,操作結束後再解鎖。也就是說,使用了互斥鎖,同一時刻只能有一個 goroutine 在執行。

相關文章