72
61

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

go言語初心者が図を書きながらgoroutineやgo channelを理解する(Part2)

Last updated at Posted at 2016-04-12

前提

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言語講座にある図を引用します。
unbuffered channelのイメージ
左右に[GR]というシャツを着て立っている人はgoroutineです。ステップ2では、左のgoroutineがchannelに値を入れているところで、右側のgoroutineが値を取り出すまで(ステップ6まで)ロックされた状態です。

ちなみにGo in Actionでもほぼ同じ図で解説されていて、channelの振る舞いが良く再現されていると思います。

私は最初にこの絵を見た時に、たかがchannelで値を渡すだけなのに、こんなに6コマも何で必要なの?と思いました。
でもchannelのブロック状態を意識しないと、すぐにロックの罠にはまります。

まずは上の図にあるように、シンプルに1つのチャネルに値を渡して取り出します。

エラーする例を示します。

デッドロックするtry_gochannel.go
package main

import "fmt"

func SimpleShori() {
    myCh := make(chan int)
    myCh <- 100 //channelに書き込み
    num := <-myCh //channelから値を取り出し
    fmt.Println(num)
}
func main(){}

実行結果を見るためのテストコードは以下のとおりです。

try_gochannel_test.go
package main
    
import "testing"
    
func TestSimpleShori(t *testing.T) {
    SimpleShori()
}

go test -vで実行するとdead lockが発生します。
当たり前なのですが、myCh<-100とした時点で、他のgoroutineが値を取り出すまではsleepして後の処理が実行されません。
上にある人間の図だと、自分で箱に値を突っ込んで、ロックされつつも、箱の反対側から手を突っ込んで値を取り出そうとしているけどできない、感じでしょうか。

シーケンス図ではこんな感じ。(かなり無理やりに表現しているので、厳密ではありません。。。)

SimpleNGGoChannel.png

上にある人間の図を再現するにはもう1つgoroutineが必要です。
無理やりシーケンス図で書くと以下のような感じです。

SimpleGoChannel.png

修正後try_gochannel.go
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に書き込みます。

またまた強引にシーケンス図にすると以下のようになります。

2GoChannels.png

try_gochannel.go
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() {
}
try_gochannel_test.go
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)を実行します。

catchandthrowGoChannels.png

(注意:readとwriteの順番が逆になることがあるので、毎回readはchannelに値が入るまで待つ(同期的)にはなりませんが、無理矢理イメージをシーケンス図で表現しています)

try_gochannel.go
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() {
}
try_gochannel_test.go
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の処理が綺麗に可視化されていてとてもおすすめです。英語版はこちら

Golang channels tutorial

Block Rockin' Codes: Goの並行処理
こちらもまとまってて、何回も読み返しました。

Ardan labs Training
絵を参考にしました

72
61
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
72
61

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?