goのWaitGroupでハマった話
goで並列処理をするときにsync.WautGroupを使って、Goroutineが終わるのを待つ処理を書いて、意図した通りに動かず30分ほど無駄にした話です。
結論
先に結論を書いておくと、Goroutineの中でAdd
するのではなく、Goroutineを呼び出す前にAdd
をしておけば問題ないです。
func something(wg *sync.WaitGroup) { fmt.Println("hello") defer wg.Done() } func main() { var wg sync.WaitGroup for i := 0; i < 2; i++ { // これをGorutineの中でやらない wg.Add(1) go something(&wg) } fmt.Println("wait") wg.Wait() fmt.Println("end") }
なにでハマったのか
最初に書いていたコードはこんな感じ。
func something(){ fmt.Println("hello") } func main() { for i := 0; i < 2; i++ { something() } fmt.Println("end") }
これはhello
が2回表示されたあとにend
が表示されるだけの、なんの問題もないコード。
hello hello end
main
から呼んでいるsomething
をGoroutineとして実行するように、something
の呼び出し時にgo
を追加。
func something(){ fmt.Println("hello") } func main() { for i := 0; i < 2; i++ { go something() } fmt.Println("end") }
この状態だとmain
はGoroutineの終了をまたずに終了してしまうので、実行すると下記のように出力された。
end
このままだとGoroutineの処理が最後まで終わらないので、main
がsomething
が終わってから終わるようにsync.WautGroup
を使うように変更しました。
func something(wg *sync.WaitGroup) { fmt.Println("hello") wg.Add(1) defer wg.Done() } func main() { var wg sync.WaitGroup for i := 0; i < 2; i++ { go something(&wg) } fmt.Println("wait") wg.Wait() fmt.Println("end") }
これで以下のような出力になることを想定してました。
wait hello hello end
が、実際に出力されたのは下記。
wait end
hello
が出力されないままmain
が終わってしまってるようです。
試しにfor
の中でsleepするように変更する。
func something(wg *sync.WaitGroup) { fmt.Println("hello") wg.Add(1) defer wg.Done() } func main() { var wg sync.WaitGroup for i := 0; i < 2; i++ { go something(&wg) time.Sleep(1 * time.Millisecond) } fmt.Println("wait") wg.Wait() fmt.Println("end") }
hello
が出力されるようになりました。
これで一応はGoroutineが終わるまでまってmain
が終わるようになりました…
(sleepさせる時間は、少し試してみたところ1msほどsleepさせれば安定してhello
が出力された)
hello hello wait end
sleepをfor
ループの中で毎回するのは、ループの回数が増えてきた単純に遅くなっていくのでなるべく避けたいので、調べてみたら下記の記事でわかりやすく説明してくれてました。
https://qiita.com/ruiu/items/dba58f7b03a9a2ffad65
Goroutineがスケジューリングされるタイミングは即時ではなく、任意なのでGoroutineが動いてAdd
される前にWait
まで到達することがあるということで、Goroutineの中ではなく、Goroutineの呼び出し前にAdd
をしておく必要があるということでした。
最初にも載せてありますが、直したコードがこちら。
func something(wg *sync.WaitGroup) { fmt.Println("hello") defer wg.Done() } func main() { var wg sync.WaitGroup for i := 0; i < 2; i++ { wg.Add(1) go something(&wg) } fmt.Println("wait") wg.Wait() fmt.Println("end") }
今までは、Goroutineを生成する個数自体が多かったり、Goroutineを呼び出してからすぐにWait
をすることがなかったので、気づくことがなかっただけで、何回かGoroutineの中でAdd
をしているコード書いてしまっていました。
しかもそれがなんの問題もなく動いていたので、原因に気づくのにずいぶん遠回りすることになりました。
今後は気をつけて書いていこうと思います。