Go
golang

【Go】基本文法⑦(並行処理)


概要

【Go】基本文法①(基礎)

【Go】基本文法②(フロー制御文)

【Go】基本文法③(ポインタ・構造体)

【Go】基本文法④(配列・スライス)

【Go】基本文法⑤(Maps・ Range)

【Go】基本文法⑥(インターフェース)

に引き続きGoの基本を学習します。

今回は Goroutine(ゴルーチン)Channel に焦点を当てて学習しました。


goroutine(ゴルーチン)とは

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


goroutine(ゴルーチン)の起動

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

go 関数名(引数, ...)

goは新しいgoroutineを生成して、そのgoroutine内で指定された関数を実行します。

新しいgoroutineは並行に動作するので、関数の実行終了を待つことなく、goのあとに記述されているプログラムを実行します。


goroutine(ゴルーチン)の終了条件

goroutine(ゴルーチン)は下記の条件で終了します。


  • 関数の処理が終わる。


  • returnで抜ける。


  • runtime.Goexit() を実行する


存在するgoroutine(ゴルーチン)の数の取得方法

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

import( 

"fmt"
"log"
"runtime"
)

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


goroutine(ゴルーチン)の実例

実際のコードを見る事でよりgoroutine(ゴルーチン)を具体的に捉えます。

以下のプログラムは1秒間隔でstrnum回表示します。

package main

import (
"fmt"
"time"
)

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

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

この例ではgoroutine(ゴルーチン)も何も使用していないので実行結果は以下の様になります。

Start!

0 A
1 A
2 A
0 B
1 B
2 B
Finish!

そこで、新しいgoroutineを2つ生成して関数process()を実行します

func main() {

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

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

すると実行結果は以下の様になりました。

Start!

Finish!

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

関数process()の実行結果を得るためには、maingoroutineが終了するまで待たなければいけません。

これは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の同期を可能にします。


ゴルーチンの同期(例①)

いくつかの実例から上記の意味を理解します。

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の値を受け取るまで完了待ちしています。


ゴルーチンの同期(例②)

次に以下の例もみてみます。

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型の要素が渡されるまで完了待ちをしています。


ゴルーチンの同期(例③)

ここで再びgoroutine(ゴルーチン)の実例で作成した、以下のコードについてみていきます。

func main() {

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

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

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

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

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!")
}

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

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

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

Start!

0 A
0 B
1 B
1 A
2 A
2 B
Finish!


参考

My Journey of Go ⑦

Goの並列処理の動作を理解する

Go の並行処理

お気楽 Go 言語プログラミング入門

Goの並行処理 -基本のキ-

Go by Example

An introduction to using and visualizing channels in Go

GOLANGBOT.COM Part 22: Channels

GoのChannelを使いこなせるようになるための手引

ゴルーチンと並行性パターン

実践Go言語(part12)