Golangを使っているといつの間にか使っているチャンネル。
なんとなくで使っていたので1から自分で書くと色々つまずいた。
動作確認をしながら理解を深めてみます。
ツアー:tour.golang.org/concurrency
ソース:runtime/chan.go
チャンネル作成と送受信
チャンネルはルーチン(golangのスレッド)間の送受信を行うためのものです。
送信、受信処理を始めると他のルーチンの送受信を待ちます。
なのでメインルーチンのみで通信待機状態にしてしまうとエラーが出ます。
1 2 3 4 |
func main() { c1 := make(chan int) <-c1 // 受信を待つ } |
fatal error: all goroutines are asleep - deadlock!
この場合、受信待ちが発生して解決しないのでデッドロックが発生します。
デッドロックを検知するとエラーにしてくれる優しさ。
チャンネルにはキャパシティーを設定できます。
1 2 3 4 5 6 7 8 9 10 |
func main() { c1 := make(chan int, 2) c1 <- 1 c1 <- 2 println(<-c1) // 1 c1 <- 3 println(<-c1) // 2 println(<-c1) // 3 println(<-c1) // fatal error: all goroutines are asleep - deadlock! } |
送信したものをFIFO(入れた順に取り出す)で受信しています。
うまく使えば送受信の待ち時間を減らすことが出来ます。
チャンネルは以下の状況で通信待ちが発生します。
- 何も入ってない状態での受信
<-chan
- 容量いっぱいの時の送信
chan<-
容量0の場合で受信させると必ず待機状態になるので、よくサンプルコードでメインルーチンを終わらせないための処理として使われてます。
1 2 3 4 5 6 |
main(){ ... //何かしらの非同期処理 c := make(chan struct{}) //struct{}はサイズ0なのでよく使われる <- c //来ることのない受信を待ち続ける } |
ルーチン間の通信
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
func routine1(c chan int) { for { println(1, <-c) } } func main() { c := make(chan int) go routine1(c) c <- 1 c <- 2 c <- 3 /* Output: 1 1 1 2 1 3 */ time.Sleep(time.Second) } |
チャンネル待機して、受信したら出力するだけのルーティンを起動。
受け取り前にメインルーチンが終わらないようにSleepで遅延させています。
基本的に受信側はループ内で待機する書き方になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// selectを使って複数チャンネル受け付け可にする func routine2(c chan int) { for { select { case ch := <-c: println(2, ch) } } } //rangeを使って待機することもできる func routine3(c chan int) { for ch := range c { println(3, ch) } } |
ところで受信ルーチンを3つ動かすとどうなるか。
1 2 3 4 5 |
... go routine1(c1) go routine2(c1) go routine3(c1) ... |
送信値1,2,3は1つづつ出力されますが、順番やどのルーチンで受信するかは実行ごとに変わります。
これはgoroutineがParallel(並列)ではなくConcurrent(平行)処理なためです。
また、チャンネルには受信専用 <-chan
と送信専用 chan<-
もあります。
これ単体で見るとどうしようもないですが、どちらもchan
を受け入れるのでこんな風に書くことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
func routineRcv(c <-chan int) { for { println(1, <-c) } } func routineSnd(c chan<- int) { for i := 1; i <= 5; i++ { c <- i } } func main() { c := make(chan int) go routineRcv(c) go routineSnd(c) time.Sleep(1 * time.Second) } |
チャンネルとして作って送信、受信しかできないチャンネルを渡す(返す)ことで意図しないコードを書けないようになりました。
チャンネルを閉じる
使い終わったチャンネルは閉じる必要があります。
閉じたチャンネルは通信待機しなくなり、送信しようとするとpanic: send on closed channel
エラーになります。
受信側は第2引数で閉じていないかどうか取得可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
main(){ c := make(chan int, 2) var ( alive bool cval int ) c <- 1 cval, alive = <-c println(cval, alive) // 1 true c <- 2 close(c) //ここでチャンネルを閉じた cval, alive = <-c println(cval, alive) // 2 true cval, alive = <-c println(cval, alive) // 0 false } |
中身が残っている場合にはtrue
となるので全ての通信が終わってからfalse
判定。
先ほどの送受信関数を書き換えてみるとこんな感じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
func routineRcv(c <-chan int) { for { select { case ch, alive := <-c: //第二引数で存在確認 if !alive { return } println(ch) } } } func routineSnd(c chan<- int) { defer close(c) //終わったら閉じる for i := 1; i <= 5; i++ { c <- i } } |
このcloseしているかどうかの判定は単体チャンネルの場合for range
が便利。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
func main() { c := make(chan int) go func() { //受信ルーチン for _c := range c { println(_c) } println("end") }() c <- 3 c <- 1 c <- 4 close(c) } |
複数の受信待機する場合は複数のルーチンを走らせるかselect
で処理を分ける。
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 |
func main() { ch1 := make(chan int) ch2 := make(chan int) end := make(chan int) go func() { //受信ルーチン L: for { select { case i := <-ch1: println("ch1", i) case i := <-ch2: println("ch2", i) case <-end: println("loop end") break L } } println("after loop") }() go func() { //送信ルーチン ch2 <- 7 ch1 <- 77 close(end) // end <- 0 とかでも可 }() time.Sleep(time.Second) } |