前回は、GoroutineのBuffered Channelとrangeの組み合わせ、Producer/Consumerパターンを学びました。
今回も引き続き、Goroutineについて学んでいきましょう。
前回
Goroutineとデータ処理連携
立ち上げたGoroutine間でデータを受け渡し、処理を行うことが可能です。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
package main import ( "fmt" "math/rand" ) func process1(pro1 chan int) { defer close(pro1) for i := 0; i < rand.Intn(10)+1; i++ { pro1 <- i } } func process2(pro1, pro2 chan int) { defer close(pro2) for i := range pro1 { // pro1格納値を2倍にする pro2 <- i * 2 } } func process3(pro2 chan int, pro3 chan string) { defer close(pro3) for i := range pro2 { // pro2格納値に文字列を付与 pro3 <- fmt.Sprintf("得られた値: %d", i) } } func main() { // goroutin用のchannelを3つ作成する pro1 := make(chan int) pro2 := make(chan int) pro3 := make(chan string) go process1(pro1) go process2(pro1, pro2) go process3(pro2, pro3) for result := range pro3 { fmt.Println(result) } } |
上記のコードでは、値を受け渡すためのchannelと処理用の関数をそれぞれ3つ作成し、「go」キーワードでgoroutine化しています。
それぞれの関数では、「defer channel」をしており、処理完了後にchannelを閉じるようにしています。
process1関数では、1~10の擬似乱数を生成し、process2では擬似乱数を2倍、process3では文字列を付与する処理を行いました。
プログラムを実行してみましょう。
1 2 3 4 5 |
$ go run main.go 得られた値: 0 得られた値: 2 得られた値: 4 得られた値: 6 |
補足) channelの明示的な指定
channelの送受信には、「<-」キーワードを用います。
関数に引数として指定する時、このキーワードを使って以下のように記述することができます。
1 2 3 |
func process3(pro2 <-chan int, pro3 chan<- string) { ... } |
process3関数では、pro2はデータ取り出し用のchannelであるため、「<-chan」を指定することができます。
同様にpro3はデータ格納用のchannelであるため、「chan<-」を指定できます。
この指定はなくても問題ないのですが、「関数の中でchannelがどのような役割をこなすか明示的に示せる」メリットがあります。
GoroutineとChannelとSelect
Selectを使うとChannel毎に別々の処理を実行することが可能です。
イメージ的には、switch構文と近いです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
package main import ( "fmt" "time" ) func process1(ch chan<- string) { for { ch <- "Hello" time.Sleep(1 * time.Second) } } func process2(ch chan<- string) { for { ch <- "World" time.Sleep(1 * time.Second) } } func main() { ch1 := make(chan string) ch2 := make(chan string) go process1(ch1) go process2(ch2) // selectを使って、処理を振り分ける for { select { case msg1 := <-ch1: fmt.Println(msg1) case msg2 := <-ch2: fmt.Println(msg2) } } } |
1 2 3 4 5 6 7 8 9 10 11 12 |
$ go run main.go World Hello Hello World Hello World Hello World World Hello ... |
GoroutineとDefault Selection
Default Selectionは、selectに指定したcaseに引っかからない場合に実行されるセクションです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package main import ( "fmt" "time" ) func main() { tick := time.Tick(100 * time.Millisecond) boom := time.After(500 * time.Millisecond) for { select { case <-tick: fmt.Println("tick.") case <-boom: fmt.Println("BOOM!") return default: fmt.Println(" .") time.Sleep(50 * time.Millisecond) } } } |
select文の中にdefaultキーワードが追加されていますが、この事を指しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$ go run main.go . . tick. . . tick. . . tick. . . tick. . . BOOM! |
ちなみに、breakで処理を抜けることも可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package main import ( "fmt" "time" ) func main() { tick := time.Tick(100 * time.Millisecond) boom := time.After(500 * time.Millisecond) BreakLoop: for { select { case <-tick: fmt.Println("tick.") case <-boom: fmt.Println("BOOM!") break BreakLoop // 処理を抜ける default: fmt.Println(" .") time.Sleep(50 * time.Millisecond) } } fmt.Println("処理を抜けます!!!!") } |
ブレークポイント「BreakLoop」を設定しました。名前は任意です。
そして、selectのcaseの中で「break ブレークポイント」の形式で指定します。
プログラムを実行してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$ go run main.go . . tick. . . tick. . . tick. . . tick. . . tick. BOOM! 処理を抜けます!!!! |
OKですね。
GoroutineとMutex
sync.Mutexは、排他制御を行う上でとても役立つ機能です。
例えば、プログラム上で設定した変数に対して、複数のGoroutineがアクセスした場合、何らかの不具合を発生させる可能性があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
package main import ( "fmt" "time" ) func main() { m := make(map[string]int) // mに対してアクセス go func() { for i := 0; i < 10; i++ { m["key"]++ } }() // mに対してアクセス go func() { for i := 0; i < 10; i++ { m["key"]++ } }() time.Sleep(1 * time.Second) fmt.Println(m, m["key"]) } |
上記のプログラムを何度か実行すると、以下のエラーが発生します。
1 2 |
$ go run main.go fatal error: concurrent map writes |
これは、2つのGoroutineが同じ変数に対してアクセスしてしまったためです。
このような問題を解消するために、sync.Mutexを使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
package main import ( "fmt" "sync" "time" ) // Mutexを含んだStructを用意する type Counter struct { v map[string]int mux sync.Mutex } func (c *Counter) Plus(key string) { // ロックをかける c.mux.Lock() // ロック対象 c.v[key]++ // ロック解除 c.mux.Unlock() // defer c.mux.Unclock()でもOK } func (c *Counter) Get(key string) int { c.mux.Lock() defer c.mux.Unlock() return c.v[key] } func main() { m := Counter{v: make(map[string]int)} go func() { for i := 0; i < 10; i++ { m.Plus("key") } }() go func() { for i := 0; i < 10; i++ { m.Plus("key") } }() time.Sleep(1 * time.Second) fmt.Println(m, m.Get("key")) } |
Counterには、sync.Mutexをプロパティとして持たせています。
このパッケージには、「Lock」と「Unlock」メソッドが実装されており、これにより排他制御を可能にします。
プログラムを実行してみましょう。
1 2 |
$ go run main.go {map[key:20] {0 0}} 20 |
今度は、何度実行してもエラーにならないはずです。
おわりに
Goroutineはやはり便利ですね。
軽量なスレッドで、channelやMutexと組み合わせることで様々な処理を素早く、簡単に実行できます。
Goエンジニアになるためには、使いこなしたい機能ですね^^
それでは、また!
最近のコメント