いろいろ備忘録日記

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

Goメモ-67 (複数の全てのチャネルが閉じたら閉じるチャネル, WhenAll)

概要

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

C#には、複数のタスクをまとめてしまって、その内の全てが完了したら完了扱いにしてくれるタスクを返す

Task.WhenAll() というメソッドがあります。

Goの場合でも、非同期処理していると走らせている複数の処理の全部が完了したら、先に進みたいときがあります。

そのような場合に便利かもしれません。基本的に sync.WaitGroupがあるので、それをメイン側で作って渡しておいて wg.Wait() ってやるのと変わらないのですが、ちょっとしたプログラム作ってるときで一々wg作るの面倒なときとかによく利用してる関数です。

サンプル

package chans

import (
    "sync"
)

// WhenAll -- 指定した1つ以上のチャネルの全てが閉じられたら、閉じるチャネルを返します。
//
// チャネルを一つも渡さずに呼び出すと、既に close 済みのチャネルを返します。
func WhenAll(channels ...<-chan struct{}) <-chan struct{} {
    switch len(channels) {
    case 0:
        nilCh := make(chan struct{})
        close(nilCh)

        return nilCh
    case 1:
        return channels[0]
    }

    allDone := make(chan struct{})
    go func() {
        defer close(allDone)

        wg := sync.WaitGroup{}
        wg.Add(len(channels))

        for _, v := range channels {
            go func(ch <-chan struct{}) {
                defer wg.Done()
                <-ch
            }(v)
        }

        wg.Wait()
    }()

    return allDone
}

gomy/whenall.go at master · devlights/gomy · GitHub

以下、ユニットテストコードです。

package chans

import (
    "testing"
    "time"
)

func TestWhenAll(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{100 * time.Millisecond},
        },
        {
            in:  testin{makeChCount: 1},
            out: testout{150 * time.Millisecond},
        },
        {
            in:  testin{makeChCount: 2},
            out: testout{250 * time.Millisecond},
        },
        {
            in:  testin{makeChCount: 3},
            out: testout{350 * time.Millisecond},
        },
        {
            in:  testin{makeChCount: 4},
            out: testout{450 * time.Millisecond},
        },
        {
            in:  testin{makeChCount: 5},
            out: testout{550 * time.Millisecond},
        },
        {
            in:  testin{makeChCount: 6},
            out: testout{650 * 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 := <-WhenAll(chList...); ok {
                t.Errorf("want: false\tgot: %v", ok)
            }

            elapsed := time.Since(start)
            t.Logf("len(ch)=%d\telapsed=%v\n", len(chList), elapsed)

            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 ^TestWhenAll$
=== RUN   TestWhenAll
    TestWhenAll: whenall_test.go:77: len(ch)=0  elapsed=0s
    TestWhenAll: whenall_test.go:77: len(ch)=1  elapsed=100.4317ms
    TestWhenAll: whenall_test.go:77: len(ch)=2  elapsed=200.8921ms
    TestWhenAll: whenall_test.go:77: len(ch)=3  elapsed=300.023ms
    TestWhenAll: whenall_test.go:77: len(ch)=4  elapsed=401.4599ms
    TestWhenAll: whenall_test.go:77: len(ch)=5  elapsed=501.9605ms
    TestWhenAll: whenall_test.go:77: len(ch)=6  elapsed=600.0772ms
--- PASS: TestWhenAll (2.10s)
PASS
ok      github.com/devlights/gomy/chans 2.187s

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

参考

Go言語による並行処理

Go言語による並行処理

関連記事

devlights.hatenablog.com


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

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

devlights.github.io

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

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

github.com

github.com

github.com