2018年12月13日木曜日

golang のチャネルを学ぶ

概要

golang のチャネルは goroutine を使って非同期にメッセージの送受信を行うことができます
自分はキューみたいなイメージしています
このサイトが非常にわかりやすいのでこれをベースに進めます

環境

  • macOS 10.14.1
  • golang 1.11.2

準備

  • mkdir $GOPATH/github.com/hawksnowlog/chn_test
  • touch $GOPATH/github.com/hawksnowlog/chn_test/main.go

とりあえずサンプルを動かす

  • vim $GOPATH/github.com/hawksnowlog/chn_test/main.go
package main

import "fmt"

func main() {
    messages := make(chan string)
    go func() { messages <- "ping" }()
    ret := <-messages
    fmt.Println(ret)
}
  • go build github.com/hawksnowlog/chn_test
  • go install github.com/hawksnowlog/chn_test
  • $GOPATH/bin/chn_test

で「ping」と表示されます

messages というチャネルを作成します
messages は文字列の送受信を行うことができるチャネルです
そしてそのチャネルに対してアロー演算子を使ってメッセージの送受信を行うことができます
<- "ping" とすることでメッセージの送信を行います
<-messages とすることでメッセージの受信を行います

上記のサンプルでは ping を送信して送信した ping を受信して表示しています

チャンネルバッファリング

言うなれば複数のメッセージをチャネルに対して送受信できるようにする機能です

package main

import "fmt"

func main() {
    messages := make(chan string, 2)
    messages <- "ping"
    messages <- "ping2"
    ret1 := <-messages
    ret2 := <-messages
    fmt.Println(ret1)
    fmt.Println(ret2)
}

2 つのメッセージを送信することができます
受信する順番は先に入れたメッセージになります
なので「ping」->「ping2」の順番で表示されます

ちなみにメッセージを受信する前に 3 つ目のメッセージを送信すると fatal error: all goroutines are asleep - deadlock! というエラーが発生します
すでに 2 つメッセージが入っている場合は一度メッセージを受信しなければなりません

チャネル同期

メインの処理と goroutine の処理は基本非同期です
が、チャネルを使うことで goroutine 側の処理を待ってメインの処理を進めることができます

package main

import "fmt"
import "time"

func worker(messages chan bool) {
    fmt.Println("start")
    time.Sleep(5 * time.Second)
    fmt.Println("end")
    messages <- true
}

func main() {
    messages := make(chan bool, 1)
    go worker(messages)
    <-messages
}

5 秒待つ関数を goroutine で呼び出しています
その引数にチャネルを渡します
チャネルは bool 型のメッセージを 1 つ持つことができるようにします
ちなみに最後の <-messages をコメントアウトするだけで worker 側の処理を待たずしてメイン側は終了してしまいます

チャネルディレクション

チャネル間でメッセージの送受信をやり取りする際に専用の関数を準備することで型の保証を担保することができます

package main

import "fmt"

func ping(pings chan string, msg string) {
    pings <- msg
}

func pong(pings chan string, pongs chan string) {
    msg := <-pings
    pongs <- msg
}

func main() {
    pings := make(chan string, 1)
    pongs := make(chan string, 1)
    ping(pings, "ping")
    pong(pings, pongs)
    fmt.Println(<-pongs)
}

select 構文との組み合わせ

チャネルが複数ある場合に使えます
また複数のチャネルに対して goroutine を使って処理させる場合、それぞれの goroutine での処理が終了してからそれぞれの次の実行を実行することができます

package main

import "fmt"
import "time"

func main() {
    m1 := make(chan string, 1)
    m5 := make(chan string, 1)
    go func() {
        time.Sleep(1 * time.Second)
        m1 <- "m1"
    }()
    go func() {
        time.Sleep(5 * time.Second)
        m5 <- "m5"
    }()
    select {
    case msg := <-m1:
        fmt.Println(msg)
    case msg := <-m5:
        fmt.Println(msg)
    }
}

1 秒後の m1 が表示され 5 秒後の m5 が表示されるのがわかると思います
それぞれの goroutine の処理が終了するのを待つことが分かると思います
また m1, m5 にメッセージがない状態で case 分の評価がされると fatal error: all goroutines are asleep - deadlock! になるので注意してください

タイムアウト処理

goroutine と組み合わせる場合 goroutine 側の処理が異常に長くなり返ってこないことも考えられます
その場合は time.After + select 文を使ってタイムアウト処理を実現することができます

package main

import "fmt"
import "time"

func main() {
    m1 := make(chan string, 1)
    go func() {
        time.Sleep(3 * time.Second)
        m1 <- "m1"
    }()
    select {
    case msg := <-m1:
        fmt.Println(msg)
    case <-time.After(2 * time.Second):
        fmt.Println("m1 goroutine timeout")
    }
    m2 := make(chan string, 1)
    go func() {
        time.Sleep(1 * time.Second)
        m2 <- "m2"
    }()
    select {
    case msg := <-m2:
        fmt.Println(msg)
    case <-time.After(2 * time.Second):
        fmt.Println("m2 goroutine timeout")
    }
}

<-time.After という感じでアロー演算子を付与することを忘れないようにしてください
それぞれ goroutine の処理は 2 秒しか待ちません
m1 はメッセージを送信するのに 3 秒かかり m2 は 1 秒で完了します
なので結果としては m1 側はタイムアウトとなり m2 側はメッセージの受信まで完了します

ノンブロッキングなチャネル

普通のチャネルはブロッキングです
前にも紹介しましたが普通に使っていれば deadlock が発生する可能性があります
それを回避する方法もあり select の default 文を使うことで回避できます

package main

import "fmt"

func main() {
    ss := make(chan string, 1)
    bb := make(chan bool, 1)
    select {
    case msg := <-ss:
        fmt.Println(msg)
    default:
        fmt.Println("message not available")
    }
    bb <- true
    select {
    case msg := <-bb:
        fmt.Println(msg)
    default:
        fmt.Println("message not available")
    }
}

メッセージが存在しない場合には default 文側に遷移します

最後に

golang のチャネルを実際にコーディングし挙動を確認しながら学んでみました
非同期処理の実現や select 文と合わせたテクニックなど駆使することでかなりいろいろなケースに対応できるテクニックかなと思いました

紹介したサイトのチャネルのユースケースはまだまだ続くので興味があれば試してみてください
続きはここからです

0 件のコメント:

コメントを投稿