概要
小ネタ。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関連の本で、いい本って感じたものです。
- 作者:Katherine Cox-Buday
- 発売日: 2018/10/26
- メディア: 単行本(ソフトカバー)
- 作者:松尾 愛賀
- 発売日: 2016/04/15
- メディア: 単行本(ソフトカバー)
プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
- 作者:Alan A.A. Donovan,Brian W. Kernighan
- 発売日: 2016/06/20
- メディア: 単行本(ソフトカバー)
過去の記事については、以下のページからご参照下さい。
- いろいろ備忘録日記まとめ
サンプルコードは、以下の場所で公開しています。
- いろいろ備忘録日記サンプルソース置き場