待ち合わせ
数を数えずに待つ
チャネルで待つには、入れた数と取り出す数を合わせる必要がありました。処理が増えると、この数合わせが面倒になります。
sync.WaitGroup は、数を数える係を用意してくれます。
Go
import "sync"
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func() {
defer wg.Done()
process(item)
}()
}
wg.Wait() // 全部終わるまでここで待つ使うのは3つだけです。
プレーンテキスト
Add(1) これから1つ始める、と数を増やす
Done() 1つ終わった、と数を減らす
Wait() 数が0になるまで待つdefer と組み合わせる
Go
go func() {
defer wg.Done()
// 何をしても、抜けるときに必ず Done が呼ばれる
}()第4章でやった defer がここで効きます。処理の途中で return しても、Done の呼び忘れが起こりません。
Done を呼び忘れると Wait が永久に終わりません。 defer で書くのは、そのための保険です。
Add はゴルーチンの外で
Go
for _, item := range items {
wg.Add(1) // 外。正しい
go func() {
// wg.Add(1) をここに書くと間違い
defer wg.Done()
}()
}
wg.Wait()中に書くと、Add が実行される前に Wait に到達することがあります。数が0のままなので、待たずに素通りします。
始まる前に数える。これが Add を外に置く理由です。
結果をどう受け取るか
WaitGroup は完了を待つだけで、値は運びません。結果が要るなら、別に用意します。
Go
results := make([]int, len(items))
for i, item := range items {
wg.Add(1)
go func(idx int, it Item) {
defer wg.Done()
results[idx] = process(it) // 添字が違うので衝突しない
}(i, item)
}
wg.Wait()スライスの別々の場所に書くので、同時に書いても問題ありません。同じ変数に足し込むと壊れます。それが次のレッスンの話です。
引数で渡す理由
Go
go func(idx int, it Item) { ... }(i, item)ループの変数をそのまま使わず、引数で渡しています。第4章の defer で「引数はその場で確定する」と書いたのと同じ理由です。
Go 1.21 までは、ゴルーチンが動く頃にはループが先に進んで変数が変わっている、という有名な落とし穴がありました。Go 1.22 でループ変数は反復ごとに新しく作られるようになり、この問題は起きなくなっています。
それでも引数で渡す書き方を勧めるのは、何をゴルーチンに渡したのかが読んで分かるからです。外側の変数を暗黙に掴むより、渡すものを明示するほうが後から追えます。
要件
- 各要素の計算をゴルーチンで走らせる
sync.WaitGroupで全部の完了を待つAddはゴルーチンの外で呼ぶDoneはdeferで呼ぶ- 結果を元の順番のままカンマ区切りで返す
入出力例
gatherAll([2,3,4]) → "4,9,16"
gatherAll([5]) → "25"
gatherAll([0,3]) → "0,9"
gatherAll([1,2,3,4]) → "1,4,9,16"ヒント
編集 ゆめさく編集部