ゴルーチンリークを避けるためにはゴルーチンを確実に処理する必要がある。
ゴルーチンが終了するパターンは
- ゴルーチンが処理完了する場合
- エラーにより処理継続できない場合
- 停止(キャンセル)するように命令された場合
1と2は特に意識せずとも終了するので、3番の「停止(キャンセル)するように命令された場合」の処理についてまとめる。
ゴルーチンに停止命令する方法として、チャンネルを閉じたらゴルーチンを停止する方法がある。慣習としてこのようなチャンネルをdoneという命名にすることが多い。
go1.7からはcontextパッケージを使うのが一般的っぽい。contextパッケージについてはそのうち学習してまとめる。
ゴルーチンがチャネルからデータを受信する場合
チャネルから値を受け取り何かしらの処理をするゴルーチンがあるとする。
このゴルーチンを停止する例。
package main import ( "fmt" "time" ) // 値を受信する処理をdoneチャネルを使ってキャンセルする例 func main() { // doneチャネルは処理終了を伝えるためのチャネル。(=キャンセルを伝える) print := func( done <-chan interface{}, // 受信専用チャネル strings <-chan string, // 受信専用チャネル ) <-chan interface{} { terminated := make(chan interface{}) go func() { defer fmt.Println("print exited.") defer close(terminated) for { select { case s := <-strings: // stringsチャネルから受信した文字列を出力 fmt.Println(s) case <-done: // doneチャネルが閉じられたら抜ける fmt.Println("close done") return } } }() return terminated } strCh := make(chan string) done := make(chan interface{}) terminated := print(done, strCh) for _, s := range []string{"golang", "ruby", "python"} { strCh <- s } // printをキャンセルするゴルーチン // ちなみに、このゴルーチンを <-terminatedの後に生成するとプログラムはDeadlockしてしまう。 // → terminatedに値が送られる、閉じられることがないから。 go func() { // 出力をわかりやすくするため、1秒待ってdoneチャネルを閉じる。 // doneチャネルを閉じることで、printに処理の終了(キャンセル)を伝える time.Sleep(1 * time.Second) fmt.Println("Canceling print goroutine...") close(done) }() // メインゴルーチンは、terminatedチャネルが閉じられるまで待機 <-terminated fmt.Println("Done.") }
print
関数はチャネルから受信した文字列を出力するゴルーチン。
この関数にdone
チャネルも渡して、チャネルが閉じられたら処理を抜けるようにする。
実行結果
$ go run main.go golang ruby python Canceling print goroutine... close done print exited. Done.
ゴルーチンがチャネルに値を送信する場合
先程はチャネルから値を受信するゴルーチンを停止する場合だったが、次はチャネルに送信しているゴルーチンを停止する場合。
この場合でもやることは同じで、done
チャネルが閉じられたらゴルーチンの処理を終了させるだけ。
package main import ( "fmt" "math/rand" "time" ) // 値を送信する処理をdoneチャネルを使ってキャンセルする例 func main() { newRandStream := func(done <-chan interface{}) <-chan int { randStream := make(chan int) go func() { defer fmt.Println("newRandStream 終了") defer close(randStream) // doneチャネルが閉じられるまで、チャネルに値を送信し続ける for { select { case randStream <- rand.Int(): case <-done: fmt.Println("newRandStream キャンセル") return } } }() return randStream } done := make(chan interface{}) randStream := newRandStream(done) // newRandStream がチャネルに送信した値を3つ取り出したあとにdoneチャネルを閉じる for i := 1; i <= 3; i++ { fmt.Printf("%d: %d\n", i, <-randStream) } close(done) //newRandStreamのゴルーチンの出力を確認するためにSleepしてメインゴルーチンが終了しないようにする time.Sleep(1 * time.Second) }
newRandStream
は、チャネルに乱数を送信し続ける関数。この関数にdone
チャネルを渡し、閉じられたら処理を終了するようにしている。
実行結果は
$ go run main.go 1: 5577006791947779410 2: 8674665223082153551 3: 6129484611666145821 newRandStream キャンセル newRandStream 終了
まとめ
ゴルーチンを停止(キャンセル)させる方法は
- 停止命令用のチャネル、doneチャネルを用意
- ゴルーチン内でdoneチャネルが閉じられたら終了する処理を書く
- selectを使うといい感じに書ける
参考図書
Tour of Goを終えた人にオススメです!!