「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を引き起こすことがなくなります。
なるほどーチャネルってどこでも使えるのかと思ってましたが、そうでもないんですね。
勉強になりました!