いろいろ備忘録日記

主に .NET とか Go とか Python絡みのメモを公開しています。

Goメモ-66 (どれか一つのチャネルが閉じたら閉じるチャネル, WhenAny)

概要

小ネタ。結構便利なので個人的にはよく使っているパターンです。

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

チャネルの数を増やしていっても、毎回最も最初に完了するチャネルが閉じたタイミングで完了扱いになっていますね。

参考

Go言語による並行処理

Go言語による並行処理


過去の記事については、以下のページからご参照下さい。

  • いろいろ備忘録日記まとめ

devlights.github.io

サンプルコードは、以下の場所で公開しています。

  • いろいろ備忘録日記サンプルソース置き場

github.com

github.com

github.com