終了したことを他の複数のゴルーチンに伝えるのにチャネルのcloseを使う #golang

August 31, 2016

この記事はQiitaの記事をエクスポートしたものです。内容が古くなっている可能性があります。

複数のゴルーチンに対して、何かしらの処理が終了したことを伝えるのにチャネルのcloseが使えます。 これはruiさんの以下の記事にも書かれていることですが、ここでは閉じれたチャネルの挙動を見つつ、例を挙げて説明していきます。

閉じられたチャネルの挙動

閉じられたチャネルは、受信したり送信したりした場合、どのような挙動をするのでしょうか? 実際に送信や受信を行ってみて挙動を確かめてみましょう。

送信

閉じられたチャネルに対して送信してみましょう。

Go Playgroundで見る

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を握りつぶすことができます。 この方法を使う場合は、他のパニックも握りつぶしてしまわないように気をつけましょう。

Go Playgroundで見る

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

受信

閉じられたチャネルから受信してみましょう。

Go Playgroundで見る

package main

import "fmt"

func main() {
	ch := make(chan struct{})
	close(ch)
	fmt.Println("done", <-ch)
}

実行すると、受信しているように見えます。 しかし、どこから送られてくるのでしょうか?

done {}

実は、閉じられたチャネルから受信しようとすると即座にゼロ値が返ってきます。 上記の場合は、struct{}型のゼロ値が返ってきています。

また、カンマok記法を用いると、第2戻り値にチャネルが閉じられているかどうかが返ってきます。

Go Playgroundで見る

package main

import "fmt"

func main() {
	ch := make(chan struct{})
	close(ch)
	v, ok := <-ch
	fmt.Println("done", v, ok)
}

実行結果

done {} false

selectcaseで受信しても即座にそのcaseが実行されます。

Go Playgroundで見る

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チャネルというイディオムなので、覚えておくと良いと思います。

Go Playgroundで見る

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で回すとどうなるでしょうか?

Go Playgroundで見る

package main

import "fmt"

func main() {
	ch := make(chan struct{})
	close(ch)
	for v := range ch {
		fmt.Println(v)
	}
	fmt.Println("done")
}

実行結果

done

実行すると、ループが回ってないことがわかります。 つまり、チャネルに対してrangeでループを回した場合に、ループを止めるにはチャネルを閉じるしかないということです。

複数回閉じる

すでに閉じられているチャネルをもう一度閉じてみましょう。

Go Playgroundで見る

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度だけ実行する際に使う便利な型があるので、そちらを使いましょう。

Go Playgroundで見る

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パッケージの型ですので、複数のゴルーチンから呼び出しても問題ありません。

Go Playgroundで見る

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でブロードキャストしよう

前置きが長くなりましたが、処理が終了したことを複数のゴルーチンへブロードキャストしてみましょう。 前述の通り、閉じられたチャネルからは必ずゼロ値が送られてきます。 また、チャネルはバッファに何も値が入ってない場合には、受信時に処理をブロックします。 そのため、チャネルが閉じられるまでは、受信していると処理をブロックできます。 これらを利用すると、処理が終わるまでチャネルの受信で処理をブロックしておき、処理が終わるとチャネルがクローズされ、それが複数のゴルーチンに伝わるということが実現します。

それでは、具体的なコードを見てみましょう。

Go Playgroundで見る

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パッケージで見ることができます。

まとめ

閉じられたチャネルの挙動について説明し、チャネルを閉じることで複数のゴルーチンに処理が終了したことをブロードキャストする方法について説明しました。