Go語言中除了可以使用通道(channel)和互斥鎖進行兩個並發程序間的同步外,還可以使用等待組進行多個任務的同步,等待組可以保證在並發環境中完成指定數量的任務
在 sync.WaitGroup(等待組)類型中,每個 sync.WaitGroup 值在內部維護着一個計數,此計數的初始默認值為零。
等待組有下面幾個方法可用,如下所示。
- func (wg *WaitGroup) Add(delta int): 等待組的計數器 +1
- func (wg *WaitGroup) Done(): 等待組的計數器 -1
- func (wg *WaitGroup) Wait(): 當等待組計數器不等於 0 時阻塞直到變 0。
對於一個可尋址的 sync.WaitGroup 值 wg:
- 我們可以使用方法調用 wg.Add(delta) 來改變值 wg 維護的計數。
- 方法調用 wg.Done() 和 wg.Add(-1) 是完全等價的。
- 如果一個 wg.Add(delta) 或者 wg.Done() 調用將 wg 維護的計數更改成一個負數,將會產生 panic 異常。
- 當一個協程調用了 wg.Wait() 時,
- 如果此時 wg 維護的計數為零,則此 wg.Wait() 此操作為一個空操作(noop);
- 否則(計數為一個正整數),此協程將進入阻塞狀態。當以后其它某個協程將此計數更改至 0 時(一般通過調用 wg.Done()),此協程將重新進入運行狀態(即 wg.Wait() 將返回)。
等待組內部擁有一個計數器,計數器的值可以通過方法調用實現計數器的增加和減少。當我們添加了 N 個並發任務進行工作時,就將等待組的計數器值增加 N。每個任務完成時,這個值減 1。同時,在另外一個 goroutine 中等待這個等待組的計數器值為 0 時,表示所有任務已經完成。
什么意思?我們先來回憶一下之前我們為了保證子 go 程運行完畢,主 go 程是怎么做的:
package main
import (
"fmt"
"time"
)
func main() {
go func() {
fmt.Println("Goroutine 1")
}()
go func() {
fmt.Println("Goroutine 2")
}()
time.Sleep(time.Second) // 睡眠 1 秒,等待上面兩個子 go 程結束
}
我們為了讓子 go 程可以順序的執行完,在主 go 程中加入了等待。我們知道,這不是一個很好的解決方案,可以用 channel 來實現同步:
package main
import (
"fmt"
)
func main() {
ch := make(chan struct{})
count := 2 // count 表示活動的 go 程個數
go func() {
fmt.Println("Goroutine 1")
ch <- struct{}{} // go 程結束,發出信號
}()
go func() {
fmt.Println("Goroutine 2")
ch <- struct{}{} // go 程結束,發出信號
}()
for range ch {
// 每次從 ch 中接收數據,表明一個活動的 go 程結束
count--
// 當所有活動的 go 程都結束時,關閉 channel
if count == 0 {
close(ch)
}
}
}
上面的解決方案是雖然已經比較好了,但是 Go 提供了更簡單的方法:使用 sync.WaitGroup
。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2) // 因為有兩個動作,所以增加 2 個計數
go func() {
fmt.Println("Goroutine 1")
wg.Done() // 操作完成,減少一個計數
}()
go func() {
fmt.Println("Goroutine 2")
wg.Done() // 操作完成,減少一個計數
}()
wg.Wait() // 等待,直到計數為0
}
可見用 sync.WaitGroup
是最簡單的方式。
強調一下:
- 計數器不能為負值:不能使用
Add()
或者Done()
給 wg 設置一個負值,否則代碼將會報錯。 - WaitGroup 對象不是一個引用類型:在通過函數傳值的時候需要使用地址。
官方文檔看這里:https://golang.org/pkg/sync/#WaitGroup
練習題
1、寫代碼實現兩個 goroutine,其中一個產生隨機數並寫入到 go channel 中,另外一個從 channel 中讀取數字並打印到標准輸出。最終輸出五個隨機數。
package main
import (
"fmt"
"math/rand"
"sync"
)
func main() {
ch := make(chan int)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 5; i++ {
ch <- rand.Int()
}
close(ch)
}()
wg.Add(1)
go func() {
defer wg.Done()
for num := range ch {
fmt.Println("num = ", num)
}
}()
wg.Wait()
}
李培冠博客
歡迎訪問我的個人網站:
李培冠博客:lpgit.com