Go言語 並列処理
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秒間隔でstr
をnum
回表示します。
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であるch1
とch2
を作成し、mainで<-ch1
や<-ch2
が呼ばれると、bool型の要素を受信するまで完了待ちをしています。
すると、関数内でprocess()
が評価されるので、期待通りの実行結果を得る事ができました。
Discussion