🌞

Go言語 並列処理

2022/08/31に公開

Goroutine (ゴルーチン)

goroutine(ゴルーチン)とは、Go言語のプログラムで並行に実行されるもののことです。

goroutineの起動

関数 (またはメソッド) の呼び出しの前にgoを付けると、異なるgoroutineで関数を実行することができます。

go 関数名(引数, ...)
  • goは新しいgoroutineを生成して、そのgoroutine内で指定された関数を実行します
  • 新しいgoroutineは並行に動作するので、関数の実行終了を待つことなく、goのあとに記述されているプログラムを実行します

goroutineの終了条件

  • 関数の処理が終わる
  • returnで抜ける
  • runtime.Goexit()を実行する

存在するgoroutineの数の取得方法

runtime.NumGoroutine()を使用する事で現在起動しているgoroutine(ゴルーチン)の数を知ることができます。

import( 
 "fmt"
 "log"
 "runtime"
)

func main() {
    log.Println(runtime.NumGoroutine())
}

実装例

実際のコードを見る事でよりgoroutine(ゴルーチン)を具体的に捉えます。
以下のプログラムは1秒間隔でstrnum回表示します。

ex) goを使わない実装

package main

import (
    "fmt"
    "time"
)

func process(num int, str string) {
    for i := 0; i <= num; i++ {
     time.Sleep(1 * time.Second)
     fmt.Println(i, str)
    }
}

func main() {
    fmt.Println("Start!")
    process(2,"A")
    process(2,"B")
    fmt.Println("Finish!")
}

// => Start!
// => 0 A
// => 1 A
// => 2 A
// => 0 B
// => 1 B
// => 2 B
// => Finish!

ex) goを使う実装

func main() {
    fmt.Println("Start!")
    go process(2,"A") //goキーワードで関数実行するとgoroutineが生成される
    go process(2,"B")
    fmt.Println("Finish!")
}

// => Start!
// => Finish!

作成されたgoroutineの終了を待たずmainが終了しプログラム全体の処理を終了したため、 関数process()の実行結果は得られませんでした。
関数process()の実行結果を得るためには、mainがgoroutineが終了するまで待たなければいけません。
これは channel(チャネル) を使う事で簡単に実現できます。


Channel(チャンネル)

ゴルーチン間(main含む)で連携するには、 channel(チャネル) と呼ばれる機能を利用します。
channel(チャネル) は、値の交換および同期という通信機能を兼ね備えており、2つの計算処理(ゴルーチン)が予期しない状態とならないことを保証します。

channelの生成方法

チャネルはGo言語に標準で用意されているデータの一つで、スライスと同じタイプのデータ構造です。

スライスの様に組み込み関数make()を使用する事で生成できます。

ch := make(chan 型名)
ch := make(chan 型名, バッファサイズ)

チャネルに指定するバッファサイズは、チャネルにバッファ可能な容量です。このサイズが送信データの上限となります。

channel(チャネル)での値の送受信方法

channel(チャネル) を使用するには、チャネル型の変数を作成し、送信側・受信側ともに、その変数に対して何らかのデータを送受信します。

以下の例の様にチャネルオペレータの<-を用いる事で値の送受信ができます。

ch <- data    //dataをchへ送信する(vをchに書き込む)
arg := <-ch   //chから受信した変数をargへ割り当てる(chの値を読み込む)

送信がchannel<-valueで受信が<-channelです。

以下のコードがchannel(チャネル)での値の送受信の実例です。

func main() {
    //channelの作成
    messages := make(chan string)

    //作成されたchannelに値(str)を送信
    go func() { messages <- "str" }()

    //channelから値を受信
    msg := <-messages
    fmt.Println(msg) //=> str
}

ゴルーチンの同期

Goでは 受信側では常に、受信可能なデータが来るまでブロックされます
また、送信側はチャネルがバッファリングしていないときは、受信側が値を受信するまでブロックされます。

これにより、Goでは明確なロックや条件変数がなくても、goroutineの同期を可能にします。

例1

func main() {
    ch := make(chan bool)  //bool型のchannelを作成

    // ゴルーチンとして以下の関数を起動。完了時にchannelの型であるboolの値を送信する事で、チャネルへ通知。
    go func() {
        fmt.Println("Hello")
        ch <- true  // 通知を送信。値は何でも良い(boolの型であれば)
    }()

    <-ch 
    //=>Hello   
    // channelの型であるboolの値を受け取るまでの完了待ち。送られてきた値は破棄
}

上記ではbool型のchannelchを作成し、ゴルーチンとして関数func()を起動しています。

func()内ではchの型であるboolの値を与えています。

Goでは受信側では常に受信可能なデータが来るまでブロックされるので、main内でchの型であるboolの値を受け取るまで完了待ちしています。

例2

func hello(done chan bool) {  
    fmt.Println("Hello world goroutine")
    done <- true
}

func main() {  
    //bool型のchannelのdoneを生成する。
    done := make(chan bool)

    //生成したdoneを関数helloに渡す
    go hello(done)

    <-done
    //main
    fmt.Println("main function")
}

上記の例ではbool型のchanneldoneを作成し、関数hello()に渡しています。

これによってmain内で<-doneが呼ばれた際に、関数hello()bool型の要素が渡されるまで完了待ちをしています。

例3

以下の実装では、作成されたgoroutineの終了を待たずmainが終了しプログラム全体の処理を終了したため、 関数process()の実行結果は得られませんでした。

func process(num int, str string) {
    for i := 0; i <= num; i++ {
     time.Sleep(1 * time.Second)
     fmt.Println(i, str)
    }
}

func main() {
    fmt.Println("Start!")
    go process(2,"A") //goキーワードで関数実行するとgoroutineが生成される
    go process(2,"B")
    fmt.Println("Finish!")
}

// => Start!
// => Finish!

これをchannelに関して学んだ知識を踏まえて以下の様に書き換えます。

func process(num int, str string) {
    for i := 0; i <= num; i++ {
     time.Sleep(1 * time.Second)
     fmt.Println(i, str)
    }
}

func main() {
    ch1 := make(chan bool)
    ch2 := make(chan bool)

    fmt.Println("Start!")

    go func() {
        process(2,"A")
        ch1 <- true
    }()

    go func(){
        process(2, "B")
        ch2 <- true
    }()

    <-ch1
    <-ch2

    fmt.Println("Finish!")
}

// => Start!
// => 0 A
// => 0 B
// => 1 B
// => 1 A
// => 2 A
// => 2 B
// => Finish!

上記ではbool型のchannelであるch1ch2を作成し、mainで<-ch1<-ch2が呼ばれると、bool型の要素を受信するまで完了待ちをしています。

すると、関数内でprocess()が評価されるので、期待通りの実行結果を得る事ができました。

Discussion