上次的文章講了 WaitGroup 的基本應用,可以參考:Golang 教學系列 - 何謂 WaitGroup? 等待 Goroutine 的好幫手!
今天這篇文章要講的是關於 WaitGroup 還有哪些應用方式以及會出現 error 的情況還有如何去避免。如果想要看更詳細的講解可以搭配我的影片來看這篇文章:Golang 教學系列 - WaitGroup 常見的坑以及應用介紹!| 肯尼攻城獅
常見 WaitGroup 錯誤及應用
當 WaitGroup 等待 Goroutine 與實際 Goroutine 數目不一致
以上次的例子來看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package main
import ( "fmt" "sync" )
func main() { wg := new(sync.WaitGroup) jobs := 100 wg.Add(jobs) for i := 1; i <= 100; i++ { go doTask(wg) } wg.Wait() fmt.Println("jobs all done!") }
func doTask(wg *sync.WaitGroup) { defer wg.Done() }
|
如果在 Add
那邊改成 (jobs + 1) 的話在重新運行程式會得到以下的錯誤:
1
| fatal error: all goroutines are asleep - deadlock!
|
這是因為,實際上 Goroutine 只有一百個,然而還記得 WaitGroup 這邊 Wait 會進行 block 住的動作,而每一次 Goroutine 做完事情都會呼叫 Done
來減一,但是最後會一直無法扣掉最後一次,導致 main 一直被 block 住,造成 deadlock 的情況產生。
多呼叫 WaitGroup.Done ()
也就是說 Add 的時候加一百次的話,但是 Done () 卻被呼叫 101 次,裡面的數目就會變成負一,執行程式的話就會出現以下錯誤:
1
| panic: sync: negative WaitGroup counter
|
這邊跳出的 panic 就很明顯告知你錯誤,WaitGroup 的 counter 不能為負數。
WaitGroup 可以重複使用
當 WaitGroup.Wait () 這個 block 被解除之後,是可以重複利用這個 WaitGroup 的,就等於是重新計算的意思,就可以重新使用 Add
來加完之後再重新 Wait。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| package main
import ( "fmt" "sync" )
func main() { wg := new(sync.WaitGroup) jobs := 100 wg.Add(jobs) for i := 1; i <= 100; i++ { go doTask(wg) } wg.Wait() fmt.Println("jobs all done!")
wg.Add(1) go func() { defer wg.Done() fmt.Println("Hello") }() wg.Wait() }
func doTask(wg *sync.WaitGroup) { defer wg.Done() }
|
來看多個 WaitGroup 的應用
直接上程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| package main
import ( "fmt" "math/rand" "sync" "time" )
var boss sync.WaitGroup var supervisor1 sync.WaitGroup var supervisor2 sync.WaitGroup
func work1() { defer supervisor1.Done() workSecond := rand.Intn(10) + 1 time.Sleep(time.Duration(workSecond) * time.Second) fmt.Println("one work1 done") }
func work2() { defer supervisor2.Done() workSecond := rand.Intn(10) + 1 time.Sleep(time.Duration(workSecond) * time.Second) fmt.Println("one work2 done") }
func wait1() { defer boss.Done() supervisor1.Wait() fmt.Println("all work1 done") }
func wait2() { defer boss.Done() supervisor2.Wait() fmt.Println("all work2 done") }
func waitAllWork() { go wait1() go wait2() boss.Wait() fmt.Println("all work done") }
func main() { supervisor1.Add(3) for i := 1; i <= 3; i++ { go work1() }
supervisor2.Add(3) for i := 1; i <= 3; i++ { go work2() }
boss.Add(2) waitAllWork()
fmt.Println("let's celebrate!") }
|
- boss 假設是老闆的 WaitGroup
- supervisor1 假設是主管一的 WaitGroup
- supervisor2 假設是主管二的 WaitGroup
功用如下:
- boss 這個 WaitGroup 是負責管理下兩個主管的工作是否完成
- supervisor1 and 2 是負責各自員工下得工作是否完成
來細看主程式的話:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| func main() { supervisor1.Add(3) for i := 1; i <= 3; i++ { go work1() }
supervisor2.Add(3) for i := 1; i <= 3; i++ { go work2() }
boss.Add(2) waitAllWork()
fmt.Println("let's celebrate!") }
|
- 每個主管都有三個 Goroutine
- 老闆必須等這兩個主管底下的 Goroutine 都完成後才可以慶功
- 所以每個主管透過 for loop 一一開啟 goroutine
- 而 work1 跟 work2 都是透過 rand 產生數字來模擬工作多久的方式,而各自用 WaitGroup.Done 的方式來通知主管這個員工做完事情了!
- 而 wait1 and wait2 則是如果主管都完成工作了,就通知 boss,也就是透過呼叫 Done () 來減一。
- 所以 waitAllWork () 這邊就會進行 boss.Wait () 來進行 block 住,而當不會被 block 住就代表全部的工作就全部被完成了。
執行以上程式就會出現以下結果:
1 2 3 4 5 6 7 8 9 10
| one work2 done one work1 done one work1 done one work2 done one work2 done all work2 done one work1 done all work1 done all work done let's celebrate!
|
總結
以上就是 WaitGroup 常見的錯誤及應用,所以到這邊可以知道如果要進行 block 住的動作除了 time.Sleep、還有就是 WaitGroup,那下一篇文章要講的就是 channel 的應用,channel 才能玩出更多樣的方式。
最後最後!請聽我一言!
如果你還沒有註冊 Like Coin,你可以透過我的邀請註冊連結來免費註冊,註冊完後就可以在文章最下方幫我按下 Like 按鈕,而 Like 最多可以點五次,如此一來你不用付出任何一塊錢,就能給我寫這篇文章最大的回饋!