「Go言語による並行処理」を写経してるんですが、チャネルのところで理解が甘かったところがありました。
やってみて、ああそうだったのか!?と気づきました。
わかってなかったことは下記の2つ
- チャネルってgoroutineで使うもの。
- そのgoroutineの中でcloseしても大丈夫(むしろこうするべき)
いやーいい加減に理解してはダメですね。
チャネルを閉じて値を取得してみる
サンプルコードを見てみましょう。「Go言語による並行処理」から抜粋してみます。
サンプル01
https://play.golang.org/p/AjTlu6R4K-p
package main import "fmt" func main() { intStream := make(chan int) close(intStream) integer, ok := <- intStream fmt.Printf("(%v): %v", ok, integer) }
結果は下記になります
(false): 0
なにも値をいれてないチャネルから値を取り出しても大丈夫だよ、ということです。
main() でチャネルに値を入れてみる
ではここで intStream チャネルになにか値を入れてみましょう。
そのために close(intStream) もdefer にします。
サンプル02
https://play.golang.org/p/3EemcCFsELK
package main import "fmt" func main() { intStream := make(chan int) defer close(intStream) intStream <- 3 integer, ok := <- intStream fmt.Printf("(%v): %v", ok, integer) }
結果は下記になります。
fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send]: main.main() /tmp/sandbox674957142/main.go:8 +0x80
え!?と思いましたがゴルーチン以外で値を入れるとデッドロックになるんですね!
ゴルーチンから値を入れてみる
ではここで intStream チャネルをgoroutine から値を入れてみます。
サンプル03
https://play.golang.org/p/ZZRzjnqnz2v
package main import "fmt" func main() { intStream := make(chan int) defer close(intStream) go func() { intStream <- 3 }() integer, ok := <- intStream fmt.Printf("(%v): %v", ok, integer) }
結果です
(true): 3
うまくいきましたね。
ゴルーチンでチャネルを閉じてみる
ではここで defer close(intStream) を goroutine に移してみましょう。
予想として、goroutine の中でチャネルを閉じてしまったら受け取るときにエラーなるんじゃないか!?っと思ってました。
だってチャネルを閉じてるんだもん。
サンプル04
https://play.golang.org/p/BfrB7K1g8QR
package main import "fmt" func main() { intStream := make(chan int) go func() { defer close(intStream) intStream <- 3 }() integer, ok := <- intStream fmt.Printf("(%v): %v", ok, integer) }
結果です
(true): 3
エラーにならず正常な結果ですね!
待受け側 for ... range チャンネルは事前にチャンネルのcloseが必要
あともう一つチャンネルの close で気になった性質。
チャンネルに値を入れた後に、そのチャンネルから値を取得する場合って for ... range で値を取得する場合がありますよね。
下記のサンプルを見てみてください。
サンプル05
https://play.golang.org/p/aA0t3EVTCxn
package main import "fmt" func main() { // 値が入ったチャンネルを返す関数 f := func() chan int{ // チャンネル作って intStream := make(chan int) // ゴルーチンの中で値入れて go func() { defer close(intStream) intStream <- 3 }() // チャンネルを返す return intStream } ch := f() // range で値を取得 for integer := range ch { fmt.Printf("%v ", integer) } }
3
結果はちゃんと取得できます。
この時ゴルーチンの中のチャンネルcloseしなかったらどうなるのかな?と思いました。
サンプル06
https://play.golang.org/p/PYvnH83M8Oe
package main import "fmt" func main() { // 値が入ったチャンネルを返す関数 f := func() chan int{ // チャンネル作って intStream := make(chan int) // ゴルーチンの中で値入れて go func() { //defer close(intStream) <-- これな!! intStream <- 3 }() // チャンネルを返す return intStream } ch := f() // range で値を取得 for integer := range ch { fmt.Printf("%v ", integer) } }
結果はこれでした
3 fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan receive]: main.main() /tmp/sandbox416365219/main.go:23 +0x100
デッドロックエラーになるんですね。
考えてみればそりゃそうですよね。for で値取得をずーっと待つことになるんですから終わりきらないのでdeadlock。当たり前だ。
ではこの値取得を <- で取得したらどうなるのか?
サンプル07
https://play.golang.org/p/d9AjTBsc3Q6
package main import "fmt" func main() { // 値が入ったチャンネルを返す関数 f := func() chan int{ // チャンネル作って intStream := make(chan int) // ゴルーチンの中で値入れて go func() { //defer close(intStream) <-- これな!! intStream <- 3 }() // チャンネルを返す return intStream } ch := f() // <- で値を取得 num := <- ch fmt.Printf("%v ", num) }
3
値はちゃんと取れますね。<- で値を取得するだけであればチャンネルをcloseしなくても値取得できるんですね。
チャンネルをちゃんと閉じないのはあまりよろしくないですが。。。
チャネル所有するゴルーチンはチャネルを閉じる責任がある
「Go言語による並行処理」のP77では、ざっくりですが、チャネル所有するゴルーチンはチャネルを閉じる責任があるそうです。
そうすることでチャネル所有者がチャネルを閉じるので、閉じたチャネルに書き込んでしまったり、2度チャネルを閉じてしまったり、といったpanicを引き起こすことがなくなります。
なるほどーチャネルってどこでも使えるのかと思ってましたが、そうでもないんですね。
勉強になりました!
- 作者: Katherine Cox-Buday,山口能迪
- 出版社/メーカー: オライリージャパン
- 発売日: 2018/10/26
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る