いろいろ備忘録日記

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

Goメモ-53 (チャネル (channel), Goroutines, Tour of Go)

概要

Tour of Go のメモをしていたけど、書いたことをすっかり忘れていたので勿体ないのでここにメモ。

Tour of Go の - Channels についてのサンプル。

tour.golang.org

チャネルは非同期処理で安全に使える型付きのキューみたいなイメージです。

Goでの非同期処理の基本はゴルーチン同士でチャネルを使ってデータをやり取りする形です。

有名な言葉がこれですね。

Do not communicate by sharing memory; instead, share memory by communicating.

golang.org

ちょっと構文が特殊に見えますが、慣れると分かりやすくていいです。

サンプル

package tutorial

import (
    "fmt"
    "sync"
    "time"
)

// Channels は、 Tour of Go - Channels (https://tour.golang.org/concurrency/2) の サンプルです。
func Channels() error {
    // ------------------------------------------------------------
    // Go言語のチャネル型(Channel)は、チャネルオペレータ(<-)を用いて値の送受信を
    // 行うためのストリーム(通り道)となる。
    //
    // 送受信用、送信用、受信用とそれぞれのチャネルを構築して利用できる.
    //
    // チャネルは、スライスやマップと同じく 組み込み関数 make() を用いて生成する
    //
    // 初期値はnil。 nil なチャネルにアクセスすると ランタイムエラーが発生するので注意
    //
    // チャネルは、 組み込み関数 close() を利用することにより閉じることが可能
    // 閉じたチャネルは、書き込みは出来ないが読み込みは行える
    // チャネルの性質を利用することにより、他の言語のシグナルと同じような動きを実現出来る
    // (専用の型が sync.Cond として用意されているがチャネルを使っても出来る)
    //
    // チャネルは、FIFOのキューのような構造をしている。通常、片方が準備できるまで
    // 送受信はブロックされる。データが送信されると受信できるようになり、その逆もしかり。
    //
    // チャネルは、 chan 型 という形で定義する。
    //   - ch := make(chan int)
    // 上は、intの値をやり取りできる送受信用のチャネルを生成している。
    //
    // 送信用と受信用も以下のようにして定義できる
    //   - sendCh chan<- int := ch
    //   - recvCh <-chan int := ch
    //
    // つまり、矢印の向きにデータが流れる
    //
    // チャネルと利用することにより、明確なロックや条件変数がなくても、goroutineの同期を
    // とることが出来る.
    //
    // チャネルは、forループで処理することも出来て、range チャネル とすることで
    // closeされるまで、データを受信し続けることも出来る.
    // chan + for-loop + select の組合せはとても良く使うパターン。
    //
    // チャネルは、バッファとして利用できる。バッファ付きのチャネルを作るには
    // makeの引数にバッファサイズを付与する。
    //   - make(chan int, 4)
    // バッファが詰まっている場合、送信がブロックされる。
    // バッファが空の場合、受信がブロックされる。
    // (C# の BlockingCollection みたいな感じ)
    //
    // チャネルが close されているかどうかは、以下の構文で判定できる
    //         if v, ok := <-ch; !ok {
    //             // closeされている
    //         }
    // ------------------------------------------------------------
    var (
        ch1 = make(chan interface{})
    )

    run := func(wait time.Duration, prefix string, done chan<- interface{}) {
        if done != nil {
            defer func() {
                done <- struct{}{}
            }()
        }

        fmt.Printf("[%s] func begin\n", prefix)
        time.Sleep(wait)
        fmt.Printf("[%s] func end\n", prefix)
    }

    go run(2*time.Second, "async", ch1)
    run(1*time.Second, "sync", nil)

    // 待ち合わせ
    <-ch1

    // 送受信用のチャネルを作って、それを送信用と受信用にそれぞれ分けて使う
    var (
        calcCh            = make(chan int)
        sendCh chan<- int = calcCh
        recvCh <-chan int = calcCh
        data              = []int{7, 2, 8, -9, -4, 0}
        sum               = func(d []int, c chan<- int) {
            result := 0
            for _, v := range d {
                result += v
            }

            c <- result
        }
    )

    // 2つの goroutine で並行処理
    go sum(data[:len(data)/2], sendCh)
    go sum(data[len(data)/2:], sendCh)

    // 結果を取得
    x, y := <-recvCh, <-recvCh

    fmt.Printf("[sum] x:%v\ty:%v\tsum:%v\n", x, y, x+y)

    // チャネルとforループ
    var (
        loopCh  = make(chan int)
        putData = func(c chan int) {
            defer close(c) // 最後にclose()を呼ぶことでこのチャネルを閉じて、これ以上データが流れることはないことを通知
            for i := 0; i < 5; i++ {
                time.Sleep(1 * time.Second)
                fmt.Printf("[send] %v\n", i)
                c <- i
            }
        }
    )

    // データを作って流す
    go putData(loopCh)

    // 流れてくるデータを出力
    // このループは、対象となる ch が close されることにより break される
    for v := range loopCh {
        fmt.Printf("[recv] %v\n", v)
    }

    // チャネルの close を利用して、待機しているgoroutineを一気に開放する
    // sync.WaitGroupやsync.Cond使っても同じことできるが一応サンプルとして。
    var (
        wg    sync.WaitGroup
        ready = make(chan interface{})
        f     = func(c <-chan interface{}, wg *sync.WaitGroup, prefix string) {
            defer wg.Done()
            fmt.Printf("[%s] wait....\n", prefix)
            <-c
            fmt.Printf("[%s] awake\n", prefix)
        }
    )

    wg.Add(3)
    go f(ready, &wg, "f1")
    go f(ready, &wg, "f2")
    go f(ready, &wg, "f3")

    fmt.Println("close(f) after 2 seconds later")
    time.Sleep(2 * time.Second)
    close(ready)
    wg.Wait()

    // チャネルが close されているかどうかは以下で判定できる
    if _, ok := <-ready; !ok {
        fmt.Println("ch [ready] was closed")
    }

    return nil
}

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

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

$ make run
ENTER EXAMPLE NAME: tutorial_gotour_channels
[Name] "tutorial_gotour_channels"
[sync] func begin
[async] func begin
[sync] func end
[async] func end
[sum] x:-13     y:17    sum:4
[send] 0
[recv] 0
[send] 1
[recv] 1
[send] 2
[recv] 2
[send] 3
[recv] 3
[send] 4
[recv] 4
close(f) after 2 seconds later
[f1] wait....
[f2] wait....
[f3] wait....
[f1] awake
[f3] awake
[f2] awake
ch [ready] was closed

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

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

devlights.github.io

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

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

github.com

github.com

github.com