Goのselect、channelの仕組みについて

Go言語

Goで並行処理をするのにselect、channelを使用しているコードを見ると思いますが、Goに馴染みがない人にとってこれは何だろうと思われるかもしれないです。
そんな方に向けに、今回この記事を読んでselect、channelの仕組みをざっくりと掴んでもらえたらと思います。
並行処理の基本となるゴルーチンから話をして、そこからchannel、selectの順で説明をしていきます。

ゴルーチン

ゴルーチンはGoの並行処理で中核になる概念
ゴルーチンはGoのランタイムによって管理されている軽いスレッドです。
複数のゴルーチンが実行されている時に、1つのプロセスから複数のスレッドが動いている状態になっているので、ゴルーチンで複数のスレッドが1つのリソースを共有している。

channel

ゴルーチンでは情報のやり取りするのにchannelを使用します。
channelを作成する時は、関数makeに、「chan 型」を指定する。

ch := make(chan int)

channelとやり取りするには演算子「<-」を使用する。channelからの読み込みにはchannel変数の左側に「<-」を書き、channelに書き込むにはchannel変数の右側に「<-」を書く

a := <-ch // channelからの読み込み。channel変数chを値をaに代入
ch <- b // channelへの書き込み。bのchannel変数chに書き込む

channelの値は一度だけ読み込まれる。1つchannelに複数のゴルーチンが読み込みをしている場合、そのうちの一つのゴルーチンからのみ読み込まれる。
ひとつのゴルーチンが同じchannelに対して読み書き両方行うのは一般的でない。
デフォルトでは、channelはバッファリングされない。オープンしてバッファリングされてないchannelに対して書き込みが行われるときに、同じchannelから他のゴルーチンが読み込みを行うまで一時停止する。またchannelを読み込んだ時に、channelへ書き込みがあるまで一時停止する。

select

ここで本題のselectについて説明します。
channelを使用すると、基本的には待ち続けることになりますが、select文はこれを補助して、並行操作の優先順を解決させます。selectを使用すると、複数channelの送受信を同時に待ち受けたり、どのchannelにもアクセスがなかった場合に処理を抜けられる。

select{
case v := <- ch1:
    fmt.Println("ch1: ", v)
case v := <- ch2:
    fmt.Println("ch2: ", v)
case ch3 := <-x:
    fmt.Println(ch3への書き込み: ", x)
}

1つのcaseに対して読み込み、書き込み操作が可能な場合に、case内の処理が実行される。
複数のchannelがある場合は、コード内の書くcaseの順番は実行の順序に関係なくランダムに実行される。そうすることで、デッドロックを防げる。例えば、2つのゴルーチンが2つのchannelにアクセスしている場合、2つのchannelは両方のゴルーチンから同じ順番でアクセスされなければデッドロックを防げる。

デッドロックになる例.

package main

import "fmt"

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)

	go func() {
		v := 1
		ch1 <- v
		v2 := <-ch2
		fmt.Println(v, v2)
	}()

	v := 2
	ch2 <- v
	v2 := <-ch1
	fmt.Println(v, v2)
}

実行したらデッドロックのエラーになる。

fatal error: all goroutines are asleep - deadlock!

selectを使用した場合

デッドロックしていたソースに対して、selectを使用
selectはcaseのいずれかを前に進めるかを確認しているのでデッドロックが回避される

package main

import "fmt"

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	go func() {
		v := 1
		ch1 <- v // vの値がch1に書き込まれる
		v2 := <-ch2
		fmt.Println("ゴルーチン:", v, v2)
	}()

	v := 2
	var v2 int

	select {
	case ch2 <- v:
		fmt.Println("ch2受信")
	case v2 = <-ch1: // ch1に書き込まれて次の処理へ進む
		fmt.Println("ch1送信")
	}

	fmt.Println("main処理", v, v2)
}

実行結果として、デッドロックされていない。
ch1の書き込み時にselectの方で処理が前に進むようになっている。

ch1送信
main処理 2 1

まとめ

channel、selectの仕組みについてまとめていきました。
channelを複数定義して、それらのchannelを複数のゴルーチンで処理をする場合はselectを使用してデッドロックを回避するようにするという認識で良いと思います。

他にもゴルーチンを同時に実行させて、1つの処理でエラーになった場合にそれを検知する方法を紹介しているのでそちらも見ていただけたらと思います。

【おすすめ記事のリンク】

コメント

タイトルとURLをコピーしました