sync.ErrGroupで複数のgoroutineを制御する
Golangの並行処理は強力である一方で同期処理を慎重に実装する必要がある.“Go 言語における並行処理の構築部材”にまとめられているようにGolangは様々な方法でそれを実装することができる.実現したいタスクに合わせてこれらを適切に選択する必要がある.
この同期処理の機構として新たにgolang.org/x/sync/errgroupというパッケージが登場した.実際に自分のツールで使ってみて便利だったので簡単に紹介する.
使いどころ
時間のかかる1つのタスクを複数のサブタスクとして並行実行しそれらが全て終了するのを待ち合わせる処理(Latch)を書きたい場合にerrgroup
は使える.その中でも「1つでもサブタスクでエラーが発生した場合に他のサブタスクを全てを終了しエラーを返したい」(複数のサブタスクが全て正常に終了して初めて1つの処理として完結する)場合が主な使いどころである.
実例
ここでは例として複数のworker
サブタスクをgoroutineで並行実行しそれらすべての終了を待ち合わせるという処理を考える.最初に今までのやりかたとしてsync.WaitGroup
を使った実装を,次にerrgroup
を使った実装を紹介する.
sync.WaitGroup
goroutineの待機処理としてよく使われるのがsync.WaitGroup
である.その名前の通り指定した数の処理(goroutine)の実行の待ち合わせに利用する.例えば以下のように書くことができる.
var wg sync.WaitGroup
errCh := make(chan error, 1)
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
worker(i)
}(i)
}
wg.Wait()
新たなgoroutineを生成する度にAdd
でWaitGroupをインクリメントし処理が終了したときにDone
を呼ぶ.そして全てのworker
の処理が終了するまでWait
で処理をブロックする.これはchannelを使っても実装できるがsync.WaitGroup
を使ったほうが読みやすいことも多い.
ではworker
でのエラーを処理をしたい場合にはどうするのが良いだろうか? sliceでエラーをため終了後にそれを取り出す,error
のchannelを作り外部でそれを受け取るといったパターンが考えられる.何にせよ別途自分で処理を実装する必要がある.
sync.ErrGroup
errgroupパッケージを使う以下のように書くことができる.
eg := errgroup.Group{}
for i := 0; i < 10; i++ {
i := i
eg.Go(func() error {
return worker(i)
})
}
if err := eg.Wait(); err != nil {
log.Fatal(err)
}
errgroup
ではGo
メソッドを使いサブタスクを実行する.ここに与えられた処理は新たなgoroutineで実行される.Wait
はsync.WaitGroup
と同様にGo
メソッドで実行された全てのサブタスクが終了するまで処理をブロックする.そして(もしあれば)Go
メソッド内で最初に返されたnon-nilのerror
を返す.
errgroup
が強力なのはcontext
パッケージを使い,1つのサブタスクでエラーが発生したときに他の全てのサブタスクをキャンセルできるところである.例えば以下のように書くことができる.
eg, ctx := errgroup.WithContext(context.TODO())
for i := 0; i < 10; i++ {
i := i
eg.Go(func() error {
return workerContext(ctx, i)
})
}
if err := eg.Wait(); err != nil {
log.Println(err)
}
違いは新たなGroupをWithContext
で生成し,かつ同時に新たなContext
も生成している部分である.またworker
をworkerContext
としContext
を渡せるようにしている.これにより1つのサブタスクでエラーが発生した場合に生成したContext
をキャンセルすることができる.つまり(workerContext
をちゃんと実装すれば)適切なリソース解放を行い処理を終了させることができる.
まとめ
これだけでなくGoDocのExampleにも挙げられているようにpipeline処理にも使うことができる.これらの処理はGolangではよく実装するパターンでありもしかしたら標準に仲間入りするかもしれない.
とりあえずサブタスクを全て実行してしまいたい,発生したエラーは全て取り出したい,といった場合は別のパターンを実装するのが良い.