🤖

🤖

:gijutsu_burogu:

読み込み、書き込みされるまで処理をブロックするチャネル(Go)

はじめに

Go ならわかるシステムプログラミングを読みました。

Goならわかるシステムプログラミング

Goならわかるシステムプログラミング

  • 作者:渋川 よしき
  • 発売日: 2017/10/23
  • メディア: 単行本(ソフトカバー)

僕は本で読みましたが、元々は Web で連載していたみたいで無料で読むことができます。(本の方がコンテンツは充実しています)

ascii.jp

Go では、goroutine という並行処理機構があります。 goroutine での並行処理には、チャネルが用いられます。

チャネル( Channel )型は、チャネルオペレータの <- を用いて値の送受信ができる通り道です。 通常、片方が準備できるまで送受信はブロックされます。これにより、明確なロックや条件変数がなくても、goroutine の同期を可能にします。 https://go-tour-jp.appspot.com/concurrency/2



わかりにくい😔😔😔

チャネルは 3 つの性質があります。

  • データを順序よく受け渡すためのデータ構造(キュー)
  • goroutine 間で正しくデータを受け渡す同期機構
  • 読み込み、書き込みの準備ができるまでブロックする機能

3 つめの、「読み込み、書き込みの準備ができるまでブロックする機能」が重要なのでまとめます。

読み込み、書き込みの準備ができるまでブロックする機能

チャネルによってブロックする例です。 goroutine 内でq <- struct{}{}されるまで<-qでブロックされます。 仮に<-qがなかった場合、一瞬で Finishが出力され main が終了します。 goroutine を制御するためにも、チャネルは必要です。

func main() {
    q := make(chan struct{})

    go func() {
        for {
            fmt.Println("Waiting...")
            rand := rand.Float64()
            fmt.Println(rand)
            if rand < 0.5 {
                q <- struct{}{} // qに入れる
            }
            time.Sleep(1 * time.Second)
        }
    }()

    <-q // q に何か入るまで待つ
    fmt.Println("Finish")
}
// Output:
// Waiting...
// 0.6046602879796196
// Waiting...
// 0.9405090880450124
// Waiting...
// 0.6645600532184904
// Waiting...
// 0.4377141871869802
// Finish

// NOTE: Goのrandパッケージはシード値を設定しないと毎回同じ乱数が吐かれます

チャネルを使ってポーリングする

チャネルを使ってポーリングする例です。 goroutine を使うことで main で別のことをしながらポーリングすることができます。

func main() {
    q := make(chan struct{}, 2)

    // 1秒ごとにポーリングしている部分
    go func() {
        for {
            fmt.Println("Waiting...")
            if requestApi() {
                q <- struct{}{}
            }
            time.Sleep(1 * time.Second)
        }
    }()

    // ポーリング中に別のことをする
    for {
        if len(q) > 0 {
            break
        }

        // q に溜まるまで他の事をしたい
        time.Sleep(time.Second)
        fmt.Println("Do something")
    }
    fmt.Println("Finish")
}

func requestApi() bool {
    time.Sleep(time.Second)
    if rand.Float64() < 0.2 {
        return true
    }
    return false
}

コンテキスト

チャネルではなく、context パッケージを使うことでも非同期処理を制御することがきます。 キャンセルやタイムアウト処理を簡単に実装することができます。

godoc.org

func main() {
    // timeoutを3秒など短い時間にするとgoroutineが完了するのが間に合わずタイムアウトする
    ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
    defer cancel()

    fmt.Println("goroutine start")
    go func() {
        for {
            if requestApi() {
                fmt.Println("goroutine done")
                cancel()
            }
        }
    }()

    <-ctx.Done()
    fmt.Println("main done")
}

func requestApi() bool {
    time.Sleep(time.Second)
    if rand.Float64() < 0.2 {
        return true
    }
    return false
}

おわりに

Go を学び始めた時は、goroutine やチャネルはよくわからなかったですが、実際に自分でコードを書いているうちに理解が進みました。 そこまで並行処理を行う機会は多くないと感じていますが、よくあるケースとしてIO処理でのブロックがある時には使えると思います。 あとは、ライブラリの実装では使われていることが多いです。

参考

mattn.kaoriya.net

Goならわかるシステムプログラミング関連記事

kotaroooo0-dev.hatenablog.com

kotaroooo0-dev.hatenablog.com