はじめに
こんにちは、キーボード自作おじさんです。このエントリはGo Advent Calendar 2017の4日目の記事です。 今年のエントリーは大作が並ぶアドベントカレンダーの休憩用のエントリーと思っていただければ幸いです。
Advent Calendar、小ネタでいいのよ。頑張り過ぎてネタ思い浮かばなくて穴開けるくらいなら575でもいいのよ。
— mattn (@mattn_jp) 2017年11月29日
Goの並列処理のパターン
Goが公開されてからもう8年になり、Goが得意とする並列処理にもGo特有のパターンなどがコミュニティ内で蓄積されてきました。 その中でもよく聞くものとしては
- for-select loop
- or-channel
- or-done-channel
- tee-channel
- fan-in, fan-out
などがあります。今日はその中でも or-done-channel について書こうと思います。
どういうときに or-done-channel を使うか
上限の数が決まっているような処理を行う場合に
- データのソースからの入力が終わってしまった場合(channelのcloseに対応)
- 上限の数に達してしまった場合(doneに対応)
のいずれかに当てはまる場合に便利なパターンです。
rangeを使う場合
まずchannelがcloseするまでの処理を行う場合は、range
キーワード使って forループを回すのが便利で可読性も高いです。
for v := range ch { something(v) }
しかし、この場合は処理の上限に達してしまった場合が対応できません。何らかの形で ch と done の両方を受けてあげる必要があります。
for-select loopだけを使う場合
そうなるとGoで複数のchannelを使った並行処理を書こうと思った場合には for-select loop が一番良く出てくるので、それを書きたくなります。
LOOP: for { select { case v, ok := <-ch: if !ok { break LOOP } something(v) case <-done: break LOOP } }
これでとりあえず希望の処理は書けるのですが、どうも見た目が大きくなってしまいます。そこでこの処理を別途まとめて、rangeの書き方に持ち込むのが or-done-pattern です。
or-done-channel
deferの性質を使って done が来ても ch からのデータ取得が終わった場合にも、かならず stream
を close しています。これで得られる stream
が閉じられたどうかだけ意識すればよくなるため、普通に range のパターンに持ち込めるわけです。
func orDone(done <-chan bool, ch <-chan Data) <-chan Data { stream := make(chan Data) go func() { defer close(stream) for { select { case <-done: return case v, ok := <-ch: if !ok { return } stream <- v } } }() return stream } for v := range orDone(done, ch) { something(v) }
Goはシンプルな文法ながら、 goroutine
、 channel
、 for
、 defer
などの機能をうまく組み合わせると非常に柔軟な書き方ができるので、こういった書き方との出会いがたくさんあって日々勉強になっています。皆さんも面白いパターンを知ったらぜひ教えてください。
明日は @cia_rana さんです。