いろいろ備忘録日記

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

Goメモ-123 (selectでのチャネル選択の有効無効を切り替える)

概要

小ネタ。select ステートメントと チャネル はよく利用される組み合わせです。

このときに、チャネルの値を nil にしたり non-nil にすることで、select ステートメントの選択対象から外したり含めたりすることが出来ます。

クローズしたチャネルが case の中に存在していると、そのチャネルに関しては常に値がゼロ値で取得できるので、そのままにしておくとものすごいスピードで select が回ったりします。

それを防ぐためにも利用できます。nil なチャネルは送信も受信も出来ないため、selectの中に書いても選択対象に入りません。

なので、無効にしたい場合は、そのチャネルの値をnilにして有効にしたい場合はnon-nilな状態にすると切り替えることができます。

実際の動作を見たほうが多分わかりやすいと思います。

サンプル

チャネルを nil にして無効にするサンプル

package goroutines

import (
    "context"
    "log"
    "os"
    "time"
)

// SelectNilChan1 -- select ステートメントで nil チャネル を使って選択されるチャネルの有効・無効を切り替えるサンプルです (1).
func SelectNilChan1() error {
    // 2つのゴルーチンを起動
    //   - 一つの生存期間は   2 秒
    //   - もう一つの生存期間は 5 秒
    // メインゴルーチンで待ち合わせをして、終わり次第結果を報告する select を一つ用意して待つ
    // メイン処理の生存期間は 7 秒とする

    // select ステートメントで case に指定するチャネルは
    // その値が nil の場合は決して選択されない。( nil チャネルは送信も受信も不可のため)
    // これを利用すると、case に指定しているチャネルの有効・無効を切り替えることが出来る
    // 無効にしたい場合は nil にして、有効にしたい場合は nil 以外の値を入れる

    // 出力用ロガー
    var (
        mainLog = log.New(os.Stdout, "[main] ", 0)
        g1Log   = log.New(os.Stderr, ">>> G1 ", 0)
        g2Log   = log.New(os.Stderr, ">>> G2 ", 0)
    )

    // 生存期間
    var (
        g1Timeout   = 2 * time.Second
        g2Timeout   = 5 * time.Second
        procTimeout = 7 * time.Second
    )

    // コンテキスト定義
    var (
        rootCtx             = context.Background()
        mainCtx, mainCancel = context.WithCancel(rootCtx)
        procCtx, procCancel = context.WithTimeout(mainCtx, procTimeout)
    )

    defer mainCancel()
    defer procCancel()

    mainLog.Println("start", time.Now().UTC().Unix())

    // ゴルーチン起動
    g1Ctx := g(procCtx, g1Timeout, g1Log)
    g2Ctx := g(procCtx, g2Timeout, g2Log)

    // 待ち合わせ
    var (
        doneProc, doneG1, doneG2 = procCtx.Done(), g1Ctx.Done(), g2Ctx.Done()
    )

LOOP:
    for {
        select {
        case <-doneG1:
            mainLog.Println("g1 done", time.Now().UTC().Unix())

            // このチャネルはクローズされているので、そのままにしておくと
            // 永遠と選択候補として残ってしまう。次から無効とするために、nil にする.
            // これにより、次から選択されなくなる.(selectの選択対象とならない)
            doneG1 = nil
        case <-doneG2:
            mainLog.Println("g2 done", time.Now().UTC().Unix())
            doneG2 = nil
        case <-doneProc:
            mainLog.Println("proc done", time.Now().UTC().Unix())
            break LOOP
        }
    }

    return nil
}

func g(pCtx context.Context, timeout time.Duration, logger *log.Logger) context.Context {
    ctx, cancel := context.WithTimeout(pCtx, timeout)

    go func() {
        defer cancel()

    LOOP:
        for {
            select {
            case <-ctx.Done():
                logger.Println("done", time.Now().UTC().Unix())
                break LOOP
            default:
            }

            logger.Println("processing...", time.Now().UTC().Unix())

            select {
            case <-ctx.Done():
            case <-time.After(1 * time.Second):
            }
        }
    }()

    return ctx
}

try-golang/selectnilchan1.go at master · devlights/try-golang · GitHub

実行すると以下のようになります。

$ make run
ENTER EXAMPLE NAME: goroutines_select_nil_chan_1
[Name] "goroutines_select_nil_chan_1"
[main] start 1605857527
>>> G2 processing... 1605857527
>>> G1 processing... 1605857527
>>> G2 processing... 1605857528
>>> G1 processing... 1605857528
>>> G1 done 1605857529
[main] g1 done 1605857529
>>> G2 processing... 1605857529
>>> G2 processing... 1605857530
>>> G2 processing... 1605857531
>>> G2 done 1605857532
[main] g2 done 1605857532
[main] proc done 1605857534


[Elapsed] 7.00053104s

上のサンプルで

           // このチャネルはクローズされているので、そのままにしておくと
            // 永遠と選択候補として残ってしまう。次から無効とするために、nil にする.
            // これにより、次から選択されなくなる.(selectの選択対象とならない)
            doneG1 = nil

の部分をコメントアウトして実行してみると、ものすごい勢いでG1のログが出力されます。クローズしたチャネルなので、常に値が取れるため何回もselectで選択される状態になります。

nilなチャネルをnon-nilにして出力を切り替えていくサンプル

package goroutines

import (
    "context"
    "fmt"
    "io/ioutil"
    "log"
    "os"
    "time"

    "github.com/devlights/gomy/chans"
)

// SelectNilChan2 -- select ステートメントで nil チャネル を使って選択されるチャネルの有効・無効を切り替えるサンプルです (2).
func SelectNilChan2() error {
    // 2つのゴルーチンを起動し、5秒毎に出力を切り替え
    // メインゴルーチンで待ち合わせをして、進捗を報告する select を一つ用意して待つ
    // メイン処理の生存期間は 12 秒、起動するゴルーチンの生存期間は 10 秒とする

    // select ステートメントで case に指定するチャネルは
    // その値が nil の場合は決して選択されない。( nil チャネルは送信も受信も不可のため)
    // これを利用すると、case に指定しているチャネルの有効・無効を切り替えることが出来る
    // 無効にしたい場合は nil にして、有効にしたい場合は nil 以外の値を入れる
    //
    // また、チャネル自体の値を入れ替えることで別のチャネルに切り替えることも可能

    // ロガー
    //
    // 有効にしたい場合は、ioutil.Discard を 望みの io.Writer に変更
    var (
        g1Log      = log.New(ioutil.Discard, ">>> [G1] ", 0)
        g2Log      = log.New(ioutil.Discard, ">>> [G2] ", 0)
        monitorLog = log.New(ioutil.Discard, ">>>>> [monitor] ", 0)
        mainLog    = log.New(os.Stdout, "[main] ", 0)
    )

    // 生存期間
    var (
        g1Timeout   = 10 * time.Second
        g2Timeout   = 10 * time.Second
        procTimeout = 12 * time.Second
    )

    // チャネル
    var (
        monitorCh  = make(chan chan string)
        g1StatusCh = make(chan string)
        g2StatusCh = make(chan string)
    )

    // コンテキスト
    var (
        rootCtx             = context.Background()
        mainCtx, mainCancel = context.WithCancel(rootCtx)
        procCtx, procCancel = context.WithTimeout(mainCtx, procTimeout)
    )

    defer mainCancel()
    defer procCancel()

    m := startMonitor(procCtx, g1StatusCh, g2StatusCh, monitorCh, monitorLog)
    g1 := startG(procCtx, "G1", g1Timeout, g1StatusCh, g1Log)
    g2 := startG(procCtx, "G2", g2Timeout, g2StatusCh, g2Log)

    // 待ち合わせながら状況を出力
    var (
        statusCh <-chan string
    )

LOOP:
    for {
        select {
        case <-procCtx.Done():
            mainLog.Println("proc done", time.Now().UTC().Unix())
            break LOOP
        case s, ok := <-monitorCh:
            if !ok {
                break LOOP
            }

            // 状況出力するチャネルを切り替え
            statusCh = s
        case v, ok := <-statusCh:
            if !ok {
                break LOOP
            }

            mainLog.Println(v)
        }
    }

    <-chans.WhenAll(m.Done(), g1.Done(), g2.Done())

    return nil
}

func startG(pCtx context.Context, name string, timeout time.Duration, statusCh chan string, l *log.Logger) context.Context {
    ctx, cancel := context.WithTimeout(pCtx, timeout)

    go func() {
        defer cancel()

        for {
            select {
            case <-ctx.Done():
                l.Println("done", time.Now().UTC().Unix())
                return
            case statusCh <- fmt.Sprintf("[%s] running... \t%d", name, time.Now().UTC().Unix()):
            }

            l.Println(time.Now().UTC().Unix())

            select {
            case <-ctx.Done():
            case <-time.After(1 * time.Second):
            }
        }
    }()

    return ctx
}

func startMonitor(pCtx context.Context, g1, g2 chan string, m chan chan string, l *log.Logger) context.Context {
    ctx, cancel := context.WithCancel(pCtx)

    go func() {
        defer cancel()

        // チャネル定義
        var (
            current  chan string // 現在処理中
            draining chan string // 吸い出し中
        )

        // コンテキスト
        var (
            drainCtx    context.Context
            drainCancel context.CancelFunc
        )

        for {
            select {
            case <-ctx.Done():
                if drainCancel != nil {
                    drainCancel()
                }

                l.Println("done", time.Now().UTC().Unix())
                return
            default:
            }

            var name string
            switch current {
            case nil:
                // 本来はここでも ctx.Done() を確認するべきだが 割愛
                m <- g1
                current = g1
                draining = g2
                name = "g2"

                drainCtx, drainCancel = context.WithCancel(pCtx)

                l.Println("prev: none\tcurrent: g1\tdraining: g2")
            case g1:
                m <- g2
                current = g2
                draining = g1
                name = "g1"

                drainCancel()
                drainCtx, drainCancel = context.WithCancel(pCtx)

                l.Println("prev: g1\tcurrent: g2\tdraining: g1")
            case g2:
                m <- g1
                current = g1
                draining = g2
                name = "g2"

                drainCancel()
                drainCtx, drainCancel = context.WithCancel(pCtx)

                l.Println("prev: g2\tcurrent: g1\tdraining: g2")
            }

            // 逆側のゴルーチンの出力を吸い出し
            drain(drainCtx, name, draining, l)

            select {
            case <-ctx.Done():
            case <-time.After(5 * time.Second):
            }
        }
    }()

    return ctx
}

func drain(pCtx context.Context, name string, ch <-chan string, l *log.Logger) {
    ctx, cancel := context.WithCancel(pCtx)
    go func() {
        defer cancel()
        for {
            select {
            case <-ctx.Done():
                l.Printf("drain %s stop\t%d", name, time.Now().UTC().Unix())
                return
            case <-ch:
                l.Printf("<<< draining %s", name)
            }
        }
    }()
}

try-golang/selectnilchan2.go at master · devlights/try-golang · GitHub

実行すると以下のようになります。

$ make run
ENTER EXAMPLE NAME: goroutines_select_nil_chan_2
[Name] "goroutines_select_nil_chan_2"
[main] [G1] running...  1605856482
[main] [G1] running...  1605856483
[main] [G1] running...  1605856484
[main] [G1] running...  1605856485
[main] [G1] running...  1605856486
[main] [G2] running...  1605856487
[main] [G2] running...  1605856488
[main] [G2] running...  1605856489
[main] [G2] running...  1605856490
[main] [G2] running...  1605856491
[main] proc done 1605856494


[Elapsed] 12.001189673s

今度は、最初 nil からスタートして状況毎にチャネルに値を設定して切り替えています。

細かいレベルのログを出力すると、以下のような感じになっています。

上のサンプルの

   // ロガー
    //
    // 有効にしたい場合は、ioutil.Discard を 望みの io.Writer に変更
    var (
        g1Log      = log.New(ioutil.Discard, ">>> [G1] ", 0)
        g2Log      = log.New(ioutil.Discard, ">>> [G2] ", 0)
        monitorLog = log.New(ioutil.Discard, ">>>>> [monitor] ", 0)
        mainLog    = log.New(os.Stdout, "[main] ", 0)
    )

の部分を

   // ロガー
    //
    // 有効にしたい場合は、ioutil.Discard を 望みの io.Writer に変更
    var (
        g1Log      = log.New(os.Stderr, ">>> [G1] ", 0)
        g2Log      = log.New(os.Stderr, ">>> [G2] ", 0)
        monitorLog = log.New(os.Stderr, ">>>>> [monitor] ", 0)
        mainLog    = log.New(os.Stdout, "[main] ", 0)
    )

に変更して実施してみます。

$ make run
[Name] "goroutines_select_nil_chan_2"
[main] [G1] running...  1605856831
>>>>> [monitor] prev: none      current: g1     draining: g2
>>> [G1] 1605856831
>>> [G2] 1605856831
>>>>> [monitor] <<< draining g2
>>> [G2] 1605856832
>>>>> [monitor] <<< draining g2
>>> [G1] 1605856832
[main] [G1] running...  1605856832
>>> [G2] 1605856833
>>>>> [monitor] <<< draining g2
>>> [G1] 1605856833
[main] [G1] running...  1605856833
>>> [G2] 1605856834
>>>>> [monitor] <<< draining g2
>>> [G1] 1605856834
[main] [G1] running...  1605856834
>>> [G2] 1605856835
>>>>> [monitor] <<< draining g2
>>> [G1] 1605856835
[main] [G1] running...  1605856835
>>>>> [monitor] prev: g1        current: g2     draining: g1
>>>>> [monitor] drain g2 stop   1605856836
>>> [G2] 1605856836
[main] [G2] running...  1605856836
>>> [G1] 1605856836
>>>>> [monitor] <<< draining g1
>>> [G1] 1605856837
[main] [G2] running...  1605856837
>>>>> [monitor] <<< draining g1
>>> [G2] 1605856837
>>> [G2] 1605856838
[main] [G2] running...  1605856838
>>> [G1] 1605856838
>>>>> [monitor] <<< draining g1
>>> [G1] 1605856839
[main] [G2] running...  1605856839
>>>>> [monitor] <<< draining g1
>>> [G2] 1605856839
[main] [G2] running...  1605856840
>>> [G1] 1605856840
>>>>> [monitor] <<< draining g1
>>> [G2] 1605856840
>>> [G1] done 1605856841
>>> [G2] 1605856841
>>> [G2] done 1605856841
[main] [G2] running...  1605856841
>>>>> [monitor] prev: g2        current: g1     draining: g2
>>>>> [monitor] drain g1 stop   1605856841
[main] proc done 1605856843
>>>>> [monitor] drain g2 stop   1605856843
>>>>> [monitor] done 1605856843


[Elapsed] 12.000959804s

おすすめ書籍

自分が読んだGo関連の本で、いい本って感じたものです。

Go言語による並行処理

Go言語による並行処理

スターティングGo言語 (CodeZine BOOKS)

スターティングGo言語 (CodeZine BOOKS)

  • 作者:松尾 愛賀
  • 発売日: 2016/04/15
  • メディア: 単行本(ソフトカバー)

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)


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

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

devlights.github.io

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

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

github.com

github.com

github.com