終了したことを他の複数のゴルーチンに伝えるのにチャネルのcloseを使う #golang
August 31, 2016
この記事はQiitaの記事をエクスポートしたものです。内容が古くなっている可能性があります。
複数のゴルーチンに対して、何かしらの処理が終了したことを伝えるのにチャネルのclose
が使えます。
これはruiさんの以下の記事にも書かれていることですが、ここでは閉じれたチャネルの挙動を見つつ、例を挙げて説明していきます。
閉じられたチャネルの挙動
閉じられたチャネルは、受信したり送信したりした場合、どのような挙動をするのでしょうか? 実際に送信や受信を行ってみて挙動を確かめてみましょう。
送信
閉じられたチャネルに対して送信してみましょう。
package main
func main() {
ch := make(chan struct{})
close(ch)
ch <- struct{}{}
}
上記のコードを動かしてみると、以下のようにパニックが発生します。
panic: send on closed channel
goroutine 1 [running]:
panic(0x9b620, 0x1040a040)
/usr/local/go/src/runtime/panic.go:500 +0x720
main.main()
/tmp/sandbox279744884/main.go:6 +0x80
panic
が発生していることからわかるように、基本的には閉じられているチャネルに対して送信することはしません。
そのため、チャネルが閉じられていることを送信する側がきちんと把握している必要があります。
しかし、どうしてもそうでない場合はrecover
してやると、panic
を握りつぶすことができます。
この方法を使う場合は、他のパニックも握りつぶしてしまわないように気をつけましょう。
package main
import "fmt"
func sendSafety(ch chan struct{}, v struct{}) (sent bool) {
defer func() {
if r := recover(); r != nil {
return
}
}()
ch <- v
return true
}
func main() {
ch := make(chan struct{})
close(ch)
fmt.Println(sendSafety(ch, struct{}{}))
}
動かしてみると、送れはしませんがpanic
も起きません。
false
受信
閉じられたチャネルから受信してみましょう。
package main
import "fmt"
func main() {
ch := make(chan struct{})
close(ch)
fmt.Println("done", <-ch)
}
実行すると、受信しているように見えます。 しかし、どこから送られてくるのでしょうか?
done {}
実は、閉じられたチャネルから受信しようとすると即座にゼロ値が返ってきます。
上記の場合は、struct{}
型のゼロ値が返ってきています。
また、カンマok記法を用いると、第2戻り値にチャネルが閉じられているかどうかが返ってきます。
package main
import "fmt"
func main() {
ch := make(chan struct{})
close(ch)
v, ok := <-ch
fmt.Println("done", v, ok)
}
実行結果
done {} false
select
のcase
で受信しても即座にそのcase
が実行されます。
package main
import "fmt"
func main() {
ch := make(chan struct{})
close(ch)
select {
case <-ch:
fmt.Println("done")
default:
fmt.Println("default")
}
}
実行結果
done
ちなみに、閉じるのではなく、nil
を入れておくと、そのcase
は実行されません。
これはnilチャネルというイディオムなので、覚えておくと良いと思います。
package main
import "fmt"
func main() {
ch := make(chan struct{})
ch = nil
select {
case v := <-ch:
fmt.Println("receive", v)
case ch <- struct{}{}:
fmt.Println("send")
default:
fmt.Println("default")
}
}
実行結果
default
range
で回すとどうなるでしょうか?
package main
import "fmt"
func main() {
ch := make(chan struct{})
close(ch)
for v := range ch {
fmt.Println(v)
}
fmt.Println("done")
}
実行結果
done
実行すると、ループが回ってないことがわかります。
つまり、チャネルに対してrange
でループを回した場合に、ループを止めるにはチャネルを閉じるしかないということです。
複数回閉じる
すでに閉じられているチャネルをもう一度閉じてみましょう。
package main
func main() {
ch := make(chan struct{})
close(ch)
close(ch)
}
panic
が発生し、閉じれないことがわかります。
panic: close of closed channel
goroutine 1 [running]:
panic(0x9b620, 0x1040a040)
/usr/local/go/src/runtime/panic.go:500 +0x720
main.main()
/tmp/sandbox820185450/main.go:6 +0x80
必ず1度しか閉じないようにするためにはどうすればよいでしょうか?
標準パッケージのsync
パッケージに、1度だけ実行する際に使う便利な型があるので、そちらを使いましょう。
package main
import (
"fmt"
"sync"
)
func main() {
ch := make(chan struct{})
var once sync.Once
for i := 0; i < 10; i++ {
once.Do(func() {
close(ch)
fmt.Println("close")
})
}
}
実行結果
close
sync.Once
は、Do
メソッドでの関数呼び出しを1度だけ行います。
これはsync
パッケージの型ですので、複数のゴルーチンから呼び出しても問題ありません。
package main
import (
"fmt"
"sync"
)
func main() {
ch := make(chan struct{})
var once sync.Once
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
once.Do(func() {
close(ch)
fmt.Println("close")
})
wg.Done()
}()
}
wg.Wait()
}
closeでブロードキャストしよう
前置きが長くなりましたが、処理が終了したことを複数のゴルーチンへブロードキャストしてみましょう。 前述の通り、閉じられたチャネルからは必ずゼロ値が送られてきます。 また、チャネルはバッファに何も値が入ってない場合には、受信時に処理をブロックします。 そのため、チャネルが閉じられるまでは、受信していると処理をブロックできます。 これらを利用すると、処理が終わるまでチャネルの受信で処理をブロックしておき、処理が終わるとチャネルがクローズされ、それが複数のゴルーチンに伝わるということが実現します。
それでは、具体的なコードを見てみましょう。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
done := make(chan struct{})
for i := 0; i < 10; i++ {
i := i
wg.Add(1)
go func() {
<-done
fmt.Println("done", i)
wg.Done()
}()
}
close(done)
wg.Wait()
}
for
文の中でdone
チャネルを受信していることがわかります。
各ゴルーチンの処理は一度ここでブロックされます。
そして、close(done)
が呼び出された時点で、<-done
からゼロ値が返されます。
そのまま各ゴルーチンの処理が再開され、画面に"done N"
(Nは数値)が表示されます。
なお、各ゴルーチンがどのような順序で実行されるかは決まっていないので、表示される順番はバラバラです。
実行結果
done 9
done 0
done 1
done 2
done 3
done 4
done 5
done 6
done 7
done 8
ここで注目したいのは、close
を1回だけ呼び出しているところです。
通常どおり、終了したことをdone <- struct{}{}
のように送ると、待機しているゴルーチンの数だけ送る必要があります。
そうすると、この処理を待っているゴルーチンの数がわからない場合などうまく使えません。
この方法は、標準パッケージにも使われており、context
パッケージで見ることができます。
- https://github.com/golang/net/blob/6250b412798208e6c90b03b7c4f226de5aa299e2/context/pre_go17.go#L182
まとめ
閉じられたチャネルの挙動について説明し、チャネルを閉じることで複数のゴルーチンに処理が終了したことをブロードキャストする方法について説明しました。