WaitGroup で goroutine の終了を待つ
複数の goroutine を起動したとき、すべてが終わるまで待ちたいことがあります。channel でも実現できますが、sync.WaitGroup を使うとよりシンプルに書けます。
WaitGroup の基本
WaitGroup は「待つべき goroutine の数」をカウントします。goroutine を起動する前にカウントを増やし、終了したらカウントを減らします。カウントが 0 になるまで待機できます。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("goroutine 完了")
}()
wg.Wait()
fmt.Println("すべて完了")
}Add(1) でカウントを 1 増やし、Done() で 1 減らします。Wait() はカウントが 0 になるまでブロックします。
3 つのメソッド
WaitGroup には 3 つのメソッドがあります。
カウントを n 増やす。goroutine を起動する前に呼ぶ。
カウントを 1 減らす。goroutine の終了時に呼ぶ。defer と組み合わせることが多い。
カウントが 0 になるまでブロックする。
Done() は Add(-1) と同じですが、意図が明確になるので Done を使うのが一般的です。
複数の goroutine を待つ
複数の goroutine を起動して、すべてを待つ例です。
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d 開始\n", id)
time.Sleep(time.Duration(id) * 100 * time.Millisecond)
fmt.Printf("Worker %d 完了\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
fmt.Println("すべての Worker が完了")
}WaitGroup を関数に渡すときは、ポインタで渡します。値渡しにするとコピーされてしまい、正しく動作しません。
Add はループの外でもよい
goroutine の数がわかっているなら、まとめて Add することもできます。
wg.Add(3)
for i := 1; i <= 3; i++ {
go worker(i, &wg)
}ただし、Add を goroutine の中で呼ぶのは避けてください。Wait がすでに呼ばれた後に Add すると panic になります。
defer wg.Done() を忘れずに
Done を呼び忘れると、Wait が永遠にブロックします。defer wg.Done() を goroutine の最初に書く癖をつけておくと安全です。
go func() {
defer wg.Done()
// 処理
}()defer を使えば、途中で return や panic が起きても Done が確実に呼ばれます。