概要
A Tour of Goの順番に沿ってGoの基本で個人的に学習したことをまとめています。
No | 記事 |
---|---|
1 | 【Go】基本文法①(基礎) |
2 | 【Go】基本文法②(フロー制御文) |
3 | 【Go】基本文法③(ポインタ・構造体) |
4 | 【Go】基本文法④(配列・スライス) |
5 | 【Go】基本文法⑤(Maps・ Range) |
6 | 【Go】基本文法⑥(インターフェース) |
7 | 〜〜 【Go】基本文法⑦(並行処理)「今ココ」〜〜 |
8 | 【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秒間隔でstr
をnum
回表示します。
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()
の実行結果を得るためには、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の同期を可能にします。
ゴルーチンの同期(例①)
いくつかの実例から上記の意味を理解します。
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)