Golang 教學系列 - WaitGroup 常見的坑以及應用介紹

上次的文章講了 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 最多可以點五次,如此一來你不用付出任何一塊錢,就能給我寫這篇文章最大的回饋!