前提
go言語初心者が図を書きながらgoroutineやgo channelを理解する(Part1)
の続きです。
まずはGo言語の並行処理の方針をEffective Goから引用します。
Do not communicate by sharing memory; instead, share memory by communicating"
つまり、ワーカーの間でメッセージパッシングを行う方法が推奨されています。
確かに共有メモリを使う方法は、直感的な気もしますけど、競合とかデッドロックとか、色々と気にしないといけないことが多くて大変です。
メッセージパッシング方式がトレンドなのかもしれません。
go channel
go channelはgoroutineの間で値を受け渡しするための配列のようなものです。
channelはバッファを持たせることができて、channelへのデータ送信時にバッファがいっぱいだとブロックしますし、データ受信時にchannelが空でもブロックします。
channelはデフォルトでunbufferedです。
ch := make(chan int) //バッファのないint型のchannel
bufferedなchannelを定義するにはsizeを割り当てます。
ch := make(chan int, 10) //size10のint型のbuffered channel
ch := make(chan, 10) //こちらは型はなしでsize10のbuffered channel
channelに値を入れる例
ch <- 1
channelから値を取り出してaに値を入れる例
a := <- ch
channelは送るだけとか、受け取るだけ、というふうに制限することもできます。
stringを送信するだけchannel
ch := make(chan <- string)
stringを受信するだけのchannel
ch := make(<- chan string)
unbufferedなchannelの使い方
unbufferedなchannelは1つの値しか格納できない配列のようなものです。
あるgoroutineがこの配列に値を入れると他のgoroutineが値を取り出すまで、他のgoroutineは配列を入れられません。
blockして配列が空になるまでsleepします。
同様に、他のgoroutineが値を取り出そうとしたときに配列が空だとblockして値が入るまでsleepします。
Ardan StudiosのGo言語講座にある図を引用します。
左右に[GR]というシャツを着て立っている人はgoroutineです。ステップ2では、左のgoroutineがchannelに値を入れているところで、右側のgoroutineが値を取り出すまで(ステップ6まで)ロックされた状態です。
ちなみにGo in Actionでもほぼ同じ図で解説されていて、channelの振る舞いが良く再現されていると思います。
私は最初にこの絵を見た時に、たかがchannelで値を渡すだけなのに、こんなに6コマも何で必要なの?と思いました。
でもchannelのブロック状態を意識しないと、すぐにロックの罠にはまります。
まずは上の図にあるように、シンプルに1つのチャネルに値を渡して取り出します。
エラーする例を示します。
package main
import "fmt"
func SimpleShori() {
myCh := make(chan int)
myCh <- 100 //channelに書き込み
num := <-myCh //channelから値を取り出し
fmt.Println(num)
}
func main(){}
実行結果を見るためのテストコードは以下のとおりです。
package main
import "testing"
func TestSimpleShori(t *testing.T) {
SimpleShori()
}
go test -v
で実行するとdead lockが発生します。
当たり前なのですが、myCh<-100
とした時点で、他のgoroutineが値を取り出すまではsleepして後の処理が実行されません。
上にある人間の図だと、自分で箱に値を突っ込んで、ロックされつつも、箱の反対側から手を突っ込んで値を取り出そうとしているけどできない、感じでしょうか。
シーケンス図ではこんな感じ。(かなり無理やりに表現しているので、厳密ではありません。。。)
上にある人間の図を再現するにはもう1つgoroutineが必要です。
無理やりシーケンス図で書くと以下のような感じです。
func SimpleShori() {
myCh := make(chan int)
go func() {
myCh <- 100
}()
num := <-myCh // channelに値が入るまで待つ
fmt.Println(num)
}
=== RUN TestSimpleShori
100
--- PASS: TestSimpleShori (0.00s)
ポイントはmyChがunbufferedであるため、無名関数はいつ実行されるかわかりませんが、num := <-myCh
はchannelに値が入るまで実行されないことです。
unbuffered channelにはgoroutine間の同期制御に使用できます。
goroutineを増やしてみます。
goroutineでTaskAとTaskBを実行します。
それぞれのタスクはunbuffered channelなmyChを読み込んでタスク名を追記して、channelに書き込みます。
またまた強引にシーケンス図にすると以下のようになります。
package main
import (
"fmt"
"time"
)
func ProcessTask(task string, c chan string) {
str := <-c
str = str + task
fmt.Println(str)
c <- str
}
func DualTaskShori() {
myCh := make(chan string)
go ProcessTask("A", myCh)
go ProcessTask("B", myCh)
myCh <- "TaskList: "
time.Sleep(time.Second)
}
func main() {
}
func TestDualTaskShori(t *testing.T) {
DualTaskShori()
}
=== RUN TestDualTaskShori
TaskList: B
TaskList: BA
--- PASS: TestDualTaskShori (1.00s)
PASS
結果は"TaskList: BA"となっているので、2つ目のgoroutine"B"から処理されたことがわかります。
1つ目と2つ目どちらのgoroutineから実行するかは、go言語のランタイムによるところなので、わかりません。
channelの待ちが見える例を示します。
channelへ値を入れる専用のgoroutine(throw)とchannelから値を取る専用のgoroutine(catch)を実行します。
(注意:readとwriteの順番が逆になることがあるので、毎回readはchannelに値が入るまで待つ(同期的)にはなりませんが、無理矢理イメージをシーケンス図で表現しています)
package main
import "testing"
func Throw(c chan<- int) { // 送る専用
for i := 0; i < 3; i++ {
c <- i
fmt.Println("Throw ", i)
}
}
func Catch(c <-chan int) { //受け取る専用
for i := 0; i < 3; i++ {
num := <-c
fmt.Println("Catch ", num)
}
}
func ThrowCatchShori() {
myCh := make(chan int)
go Throw(myCh)
go Catch(myCh)
time.Sleep(time.Second)
}
func main() {
}
func TestThrowCatchShori(t *testing.T) {
ThrowCatchShori()
}
=== RUN TestThrowCatchShori
Catch 0
Throw 0
Throw 1
Catch 1
Catch 2
Throw 2
--- PASS: TestThrowCatchShori (1.00s)
値を入れる専用のThrowも取得専用のCatchも3回for文で繰り返していますが、結果を見ると必ずThrowとCatchのペアが処理されてから、次のfor文のイテレーションへ進んでいることが見えます。
ThrowもCatchもgoroutineで、どちらが先になるかは保証されません。
buffered channelと複数のchannelの処理については次回にします。
参考サイト
Go言語の並行性を映像化する
3Dでgoroutineの処理が綺麗に可視化されていてとてもおすすめです。英語版はこちら
Block Rockin' Codes: Goの並行処理
こちらもまとまってて、何回も読み返しました。
Ardan labs Training
絵を参考にしました