channel による通信
- Go 言語には channel という機能があり、goroutine 間の通信にしばしば用いられる。
channel とは?
- Channels are a typed conduit through which you can send and receive values with the channel operator, <- (Tour of Go - Channels より抜粋)
- キュー構造 (FIFO) をしている。バッファ (容量) を任意に設定できる。
- channel への書き込みは自動的に排他制御される。
channel を使ったコード例
- Go by Example から抜粋 (playground)
- channel から読み込むとき、書き込みがあるまで待っててくれる
- channel に書き込むとき、バッファがいっぱいだったら空くまで待っててくれる
package main
import "fmt"
func main() {
messages := make(chan string)
go func() {
messages <- "ping"
}()
msg := <-messages
fmt.Println(msg)
}
package main
import (
"fmt"
"strconv"
)
func send(ch chan<- string) {
for i := 0; i < 10; i++ {
ch <- strconv.Itoa(i)
}
}
func receive(ch <-chan string) {
for i := 0; i < 10; i++ {
fmt.Println(<-ch)
}
}
func main() {
ch := make(chan string)
go send(ch)
receive(ch)
}
select を使って複数の channel を扱う
- 複数の channel を扱うときは select がしばしば用いられる。
- 複数の channel を並べて、実行可能になった channel への操作をひとつだけ実行する
- 以下の例はブロックしたまま動かないので注意 (playground)
package main
import "fmt"
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
ch3 := make(chan string)
select {
case v := <-ch1:
fmt.Println(v)
case v := <-ch2:
fmt.Println(v)
case ch3 <- "hello":
fmt.Println("hello!")
}
}
- 先述の例は一生動作しない (deadlock ということでただちに panic する)
package main
import "fmt"
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
ch3 := make(chan string)
select {
case v := <-ch1:
fmt.Println(v)
case v := <-ch2:
fmt.Println(v)
case ch3 <- "hello":
fmt.Println("hello!")
default:
fmt.Println("all channels are not available now")
}
}
余談: context による goroutine の終了処理
- goroutine を goroutine の外側から強制的に終わらせることはできない
- いわゆる pthread で言うところの cancel や kill がない
- ということで、終わらせたかったら自力でちゃんと終わらせる必要がある
- 以下、露骨に goroutine が残り続けてメモリが無駄に成る例 (goroutine leak) (playground)
package main
import "fmt"
func receive(ch <-chan string) {
fmt.Println(<-ch)
}
func main() {
ch := make(chan string)
for i := 0; i < 1000000; i++ {
go receive(ch)
}
}
- goroutine を終わらせる方法のひとつとして、
context.Context
を用いる (playground)
context.WithCancel
を用いると、context インスタンスと cancel 用の関数が得られる
- cancel 用の関数を呼び出すと、context インスタンスの
Done()
が発火する
Done()
は channel を返す関数。select で待っといて終了処理を行うことができるようになる
package main
import (
"context"
"fmt"
)
func receive(ctx context.Context, ch <-chan string) {
select {
case <-ctx.Done():
return
case v := <-ch:
fmt.Println(v)
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
ch := make(chan string)
for i := 0; i < 1000000; i++ {
go receive(ctx, ch)
}
cancel()
}