Go言語

【Go言語】doneチャネルを使ってゴルーチンに停止命令(キャンセル)したいんじゃ!!

ゴルーチンリークを避けるためにはゴルーチンを確実に処理する必要がある。

ゴルーチンが終了するパターンは

  1. ゴルーチンが処理完了する場合
  2. エラーにより処理継続できない場合
  3. 停止(キャンセル)するように命令された場合

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を終えた人にオススメです!!

イチオシ記事

1

自己紹介 フリーランスエンジニアをしているヨノと申します。 独学でプログラミングを学び、ソシャゲ・SaaS開発などを経て、2018年からフリーランスエンジニアとして活動しています。 主にバックエンド中 ...

2

はじめまして、フリーランスエンジニアのヨノと申します。 自己紹介 独学でプログラミングを学び、ソシャゲ・SaaS開発などを経て、2018年からフリーランスエンジニアとして活動しています。 主にバックエ ...

3

ネット上で色々言われているフリーランスエンジニア....。「本当はどうなの?」と思っている人は多いでしょう。 そこで本記事ではフリーランスエンジニア5年生の私が、ネット上の意見も引用しながら実態を解説 ...

-Go言語