Goの基本であるゴルーチンとチャネルについて簡単に解説してみた

thumnail
2020-09-06

Goにおけるゴルーチンとは

Goは並行処理をサポートしている。ゴルーチンは並列で処理される独立した処理単位のことだ。処理内容によってはゴルーチンを並列に並べて処理をすることで処理時間の短縮に繋がることがある。

メインルーチン

普段使っているmain関数の処理もゴルーチンだ。main関数のゴルーチンはメインルーチンと呼ばれる。メインルーチンが終了すると全てのゴルーチンが終了するという特性を持つ。

package main

import (
    "fmt"
)

func main() {
    fmt.Println("メインルーチン 開始")

    fmt.Println("メインルーチン 終了")
}

ゴルーチンを生成する方法

関数の前に予約語であるgoをつけるとその関数はゴルーチンとして並列で処理される。

go f()

func f(){
  // 処理
}

メインルーチンの中でゴルーチンを生成して並列処理をするコードを書いてみる。

$ cat main.go
package main

import (
        "fmt"
)

func f(from string) {
        for i := 0; i < 3; i++ {
                fmt.Println(from, ":", i)
        }
}

func main() {

        f("direct")

        go f("goroutine")

        // time.Sleep(time.Second)
        fmt.Println("done")
}
$ go run main.go
direct : 0
direct : 1
direct : 2
done

出力結果をみればわかるが、メインルーチン内で生成したゴルーチンを並列処理できていない。この理由は生成したゴルーチンの処理が始まる前にメインルーチンが終了してしまうので、内部のゴルーチンも終了してしまうからだ。なのでSleepメソッド使ってメインルーチンが終了するのを1秒待ってみる。

$ cat main.go
package main

import (
        "fmt"
        "time"
)

func f(from string) {
        for i := 0; i < 3; i++ {
                fmt.Println(from, ":", i)
        }
}

func main() {

        f("direct")

        go f("goroutine")

        time.Sleep(time.Second)
        fmt.Println("done")
}
$ go run main.go
direct : 0
direct : 1
direct : 2
goroutine : 0
goroutine : 1
goroutine : 2
done

メインルーチンの終了を1秒遅延させたことで、内部のゴルーチンの処理が呼ばれていることが確認できた。

ゴルーチンを使う場面

複雑な計算処理を片方のゴルーチンで実行し、その計算をしている間はアニメーションを出力するなどと言った複雑で計算に時間のかかる処理を背後で実行し、ユーザーに対しては別の処理を実行することで待ち時間を誤魔化すといった場面などが1つの例として挙げられる。

Goにおけるチャネルとは

あるゴルーチンから他のゴルーチンにデータを送信または受信するときに用いられるパイプ的な仕組みをチャネルという。チャネルを使うと他のゴルーチンにデータを送信、他のゴルーチンからデータを受信することができる。

チャネルの生成

Goでチャネルの型が用意されているのでチャネルを生成するときは以下のようにmake関数で第1引数にchanと指定することでchan int型のチャネルを生成することができる。

ch := make(chan int)

上のチャネルはバッファを持たせていない。バッファを持たないチャネルは同期チャネルという。バッファありのチャネルを生成する場合以下のプログラムのように第2引数に容量を指定する。

ch := make(chan int, 10)

チャネルの送信・受信文

あるゴルーチンに対してデータを送信、あるゴルーチンからデータを受信する処理は以下のように書きます。

// 容量5のチャネル生成
ch := make(chan int, 5)
// 送信文 (100を送信)
ch <- 100
// 受信文 
<-ch

バッファの有無による違い

バッファなしチャネル

バッファなしチャネルはデータを送信した時にすぐに受信者を必要とするという特徴がある。

$ cat main.go
package main

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

func main() {
        c := make(chan string) // バッファなしチャネル生成

        var wg sync.WaitGroup
        wg.Add(2)

        go func() {
                fmt.Println("reciver")
                defer wg.Done()

                time.Sleep(time.Second * 1)
                println(`メッセージ: ` + <-c) // 受信文 メッセージ: foo
        }()

        go func() {
                fmt.Println("sender")
                defer wg.Done()
                c <- `foo` // 送信文
        }()

        wg.Wait()
}

$ go run main.go
sender
reciver
メッセージ: foo

送信チャネルと受信チャネルを記述すればチャネル経由でデータの送受信が確認できた。では受信チャネルを定義しなかった場合どうなるのかを実験してみた。

$ cat main.go
package main

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

func main() {
        c := make(chan string) // バッファ無しチャネル生成

        var wg sync.WaitGroup
        wg.Add(2)

        go func() {
                fmt.Println("reciver")
                defer wg.Done()

                time.Sleep(time.Second * 1)
        }()

        go func() {
                fmt.Println("sender")
                defer wg.Done()
                c <- `foo`
                c <- `bar`
        }()

        wg.Wait()
}
$ go run main.go
sender
reciver
fatal error: all goroutines are asleep - deadlock!

バッファなしのチャネルで、送信文だけ書いて受信文を書かなかった場合deadlockになってしまった。これはバッファなしチャネルは受信を実行するまで送信を行うゴルーチンを待たせるという特性を持っているからだ。つまり受信はされないのに送信がずっと待機させられてしまうのでdeadlockになった。

バッファありチャネル

チャネルにバッファを持たせた場合、受信チャネルを定義しなくてもdeadlockにはならなかった。

$ cat main.go
package main

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

func main() {
        c := make(chan string, 2) // バッファ有りチャネル生成

        var wg sync.WaitGroup
        wg.Add(2)

        go func() {
                fmt.Println("reciver")
                defer wg.Done()

                time.Sleep(time.Second * 1)
        }()

        go func() {
                fmt.Println("sender")
                defer wg.Done()
                c <- `foo`
                c <- `bar`
        }()

        wg.Wait()
}

$ go run main.go
sender
reciver

これはバッファありチャネルを使用した場合は受信が実行されるまで、送信ゴルーチンを待機させるということはしないからだ。

まだまだ情報量が少ないのでこれから勉強して諸々追記していく予定。

参考文献

https://go-tour-jp.appspot.com/concurrency/3

https://medium.com/a-journey-with-go/go-buffered-and-unbuffered-channels-29a107c00268

https://program.sakaiboz.com/golang/goroutine/unbuffered-channel-and-buffered-channel/

この記事内容とは関係ありませんが、筆者である僕が普段愛用しているPC周りのアイテムを別記事で紹介しているので、もし興味があればご覧になってください。

現在使用しているディスプレイ・ヘッドフォン・キーボードなどを紹介

KATUBLO

Copyright since 2018 KATUO All Rights Reserved.