Go

sync.WaitGroupの正しい使い方

More than 3 years have passed since last update.

Goroutineを複数使って並列で処理を行って、それがすべて完了したら次に進みたいとしよう。Goroutineの完了はそれを生成したgoroutineに通知されるわけではないので、メインのgoroutineは何らかのメカニズムを使って全員が完了するまで待って、全員が完了したら実行を再開する必要がある。

sync.WaitGroupは複数のgoroutineの完了を待つための値だ(Javaを知っていれば、java.util.concurrent.CountDownLatchによく似ている)。WaitGroupの値に対してメソッドWaitを呼ぶと、WaitGroupが0になるまでWaitはブロックされる(待たされる)。従って、やりたい処理の数だけWaitGroupの値をインクリメントしておいて、処理完了時にデクリメントすれば、Waitを呼んで処理完了を待っているメインのgoroutineは、すべての処理が完了する(カウンタが0に戻る)まで待って、処理完了時に再開されるというわけだ。

というわけで概念的には単純なWaitGroupだが、マルチスレッドプログラミング特有の落とし穴があるので注意されたい。ありがちな間違いは、下のコードのように、新たに起動したgoroutineの中でWaitGroupをインクリメントしてしまうことだ。

wg := &sync.WaitGroup{}  // WaitGroupの値を作る

for i := 0; i < 10; i++ { // (例として)10回繰り返す
go func() {
wg.Add(1) // wgをインクリメント
// ここで何か処理を行う
wg.Done() // 完了したのでwgをデクリメント
}()
}
wg.Wait() // メインのgoroutineはサブgoroutine 10個が完了するのを待つ

上のプログラムは一見正しそうにみえる。Goroutineを複数起動しているが、子goroutineはまずwgをインクリメントして、完了時にデクリメントしているので、親のメインのgoroutineはすべての子が完了するまで待たされるはずだ ―― そうだよね?

しかし実際にはそうではない。Goroutineがスケジューリングされるタイミングは任意なので、goroutineが10個生成されて、しかしまだどれもまったく走っていないという状態がありえる。その状態でメインのgoroutineが最後の行に到達すると、wgは0(初期値)のままなので、メインのgoroutineはそのまま下に抜けてしまう。

このバグを直すには次のようにすればよい。いつ走るかわからないgoroutineの中でwgをインクリメントするのではなく、goroutineを起動する前にwgをインクリメントすればよいのである。

wg := &sync.WaitGroup{}  // WaitGroupの値を作る

for i := 0; i < 10; i++ { // (例として)10回繰り返す
wg.Add(1) // wgをインクリメント
go func() {
// ここで何か処理を行う
wg.Done() // 完了したのでwgをデクリメント
}()
}
wg.Wait() // メインのgoroutineはサブgoroutine 10個が完了するのを待つ

このようにすると最後の行は確実にすべてのgoroutineの完了を待つことになる。マルチスレッドプログラミングは意外な落とし穴があるが、これもその一つなので注意されたい。