概要
小ネタ。結構便利なので個人的にはよく使っているパターンです。
C#には、複数のタスクをまとめてしまって、その内のどれか一つでも完了したら完了扱いにしてくれるタスクを返す
Task.WhenAny()
というメソッドがあります。
Goの場合でも、非同期処理していると走らせている複数の処理のどれかが完了したら、とりあえず先に進みたいときがあります。
そのような場合に便利かもしれません。
サンプル
package chans // WhenAny -- 指定した1つ以上のチャネルのどれかが1つが閉じられたら、閉じるチャネルを返します。 // // チャネルを一つも渡さずに呼び出すと、既に close 済みのチャネルを返します。 func WhenAny(channels ...<-chan struct{}) <-chan struct{} { switch len(channels) { case 0: nilCh := make(chan struct{}) close(nilCh) return nilCh case 1: return channels[0] } orDone := make(chan struct{}) go func() { defer close(orDone) // 再帰呼出しの回数を抑えるために len が (2 or 3) のときは再帰せずに済ませる switch len(channels) { case 2: select { case <-channels[0]: case <-channels[1]: } case 3: select { case <-channels[0]: case <-channels[1]: case <-channels[2]: } default: select { case <-channels[0]: case <-channels[1]: case <-channels[2]: case <-WhenAny(append(channels[3:], orDone)...): } } }() return orDone }
gomy/whenany.go at master · devlights/gomy · GitHub
以下、ユニットテストコードです。
package chans import ( "testing" "time" ) func TestWhenAny(t *testing.T) { type ( testin struct { makeChCount int } testout struct { limit time.Duration } testcase struct { in testin out testout } ) cases := []testcase{ { in: testin{makeChCount: 0}, out: testout{150 * time.Millisecond}, }, { in: testin{makeChCount: 1}, out: testout{150 * time.Millisecond}, }, { in: testin{makeChCount: 2}, out: testout{150 * time.Millisecond}, }, { in: testin{makeChCount: 3}, out: testout{150 * time.Millisecond}, }, { in: testin{makeChCount: 4}, out: testout{150 * time.Millisecond}, }, { in: testin{makeChCount: 5}, out: testout{150 * time.Millisecond}, }, { in: testin{makeChCount: 6}, out: testout{150 * time.Millisecond}, }, } makeCh := func(closeAfter time.Duration) <-chan struct{} { ch := make(chan struct{}) go func() { defer close(ch) time.Sleep(closeAfter) }() return ch } for _, c := range cases { func() { chList := make([]<-chan struct{}, 0, c.in.makeChCount) for i := 0; i < c.in.makeChCount; i++ { ch := makeCh(time.Duration((i+1)*100) * time.Millisecond) chList = append(chList, ch) } start := time.Now() if _, ok := <-WhenAny(chList...); ok { t.Errorf("want: false\tgot: %v", ok) } elapsed := time.Since(start) t.Logf("len(ch)=%d\telapsed=%v\n", len(chList), elapsed) for _, v := range chList { ch := v <-ch } if c.out.limit < elapsed { t.Errorf("want: within %v\tgot %v", c.out.limit, elapsed) } }() } }
実行すると以下のようになります。
$ go test -v github.com/devlights/gomy/chans -run ^TestWhenAny$ === RUN TestWhenAny TestWhenAny: whenany_test.go:77: len(ch)=0 elapsed=0s TestWhenAny: whenany_test.go:77: len(ch)=1 elapsed=100.0049ms TestWhenAny: whenany_test.go:77: len(ch)=2 elapsed=100.988ms TestWhenAny: whenany_test.go:77: len(ch)=3 elapsed=100.0102ms TestWhenAny: whenany_test.go:77: len(ch)=4 elapsed=100.9929ms TestWhenAny: whenany_test.go:77: len(ch)=5 elapsed=100.991ms TestWhenAny: whenany_test.go:77: len(ch)=6 elapsed=100.0038ms --- PASS: TestWhenAny (2.11s) PASS ok github.com/devlights/gomy/chans 2.192s
チャネルの数を増やしていっても、毎回最も最初に完了するチャネルが閉じたタイミングで完了扱いになっていますね。
参考

- 作者:Katherine Cox-Buday
- 発売日: 2018/10/26
- メディア: 単行本(ソフトカバー)
過去の記事については、以下のページからご参照下さい。
- いろいろ備忘録日記まとめ
サンプルコードは、以下の場所で公開しています。
- いろいろ備忘録日記サンプルソース置き場