チャンネル#
単純に関数を同時に実行することには意味がありません。関数間でデータを交換する必要があり、これによって関数の同時実行の意義が示されます。
共有メモリを使用してデータを交換することは可能ですが、異なる goroutine 間で共有メモリを使用すると競合状態が発生しやすくなります。データ交換の正確性を保証するためには、ミューテックスを使用してメモリを保護する必要があり、この方法は必然的にパフォーマンスの問題を引き起こします。
Go 言語の並行モデルは CSP(Communicating Sequential Processes)であり、通信は共有メモリを介して行われるのではなく、通信を通じて共有メモリが実現されます。
もし goroutine が Go プログラムの並行実行の実体であるなら、チャンネルはそれらの間の接続です。チャンネルは、ある goroutine が特定の値を別の goroutine に送信するための通信メカニズムです。
Go 言語のチャンネルは特別な型です。チャンネルはコンベヤーベルトやキューのように、常に先入れ先出しの原則に従い、データの送受信の順序を保証します。各チャンネルは具体的な型の導管であり、チャンネルを作成する際にはその要素の型を指定する必要があります。
チャンネルの型#
チャンネルは型の一種であり、参照型です。チャンネル型の宣言形式は以下の通りです:
var 変数 chan 要素型
いくつかの例を挙げます:
var ch1 chan int // 整数型を渡すチャンネルを宣言
var ch2 chan bool // ブール型を渡すチャンネルを宣言
var ch3 chan []int // intスライスを渡すチャンネルを宣言
チャンネルの作成#
チャンネルは参照型であり、チャンネル型の空値は nil です。
var ch chan int
fmt.Println(ch) // nil
宣言されたチャンネルは、make 関数で初期化する必要があります。
チャンネルの作成形式は以下の通りです:
make(chan 要素型,[バッファサイズ])
チャンネルのバッファサイズはオプションです。
ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)
チャンネル操作#
チャンネルには送信(send)、受信(receive)、および閉鎖(close)の 3 つの操作があります。
送信と受信は両方とも <- 記号を使用します。
まず、以下の文を使用してチャンネルを定義します:
ch := make(chan int)
送信#
値をチャンネルに送信します。
ch <- 10
受信#
チャンネルから値を受信します。
x := <- ch // chから値を受信し、変数xに代入
<-ch // chから値を受信し、結果を無視
閉鎖#
組み込みの close 関数を呼び出してチャンネルを閉じます。
close(ch)
チャンネルを閉じる際に注意すべきことは、受信側の goroutine にすべてのデータが送信されたことを通知する場合にのみチャンネルを閉じる必要があるということです。チャンネルはガベージコレクションによって回収されることができますが、ファイルを閉じるのとは異なり、操作を終了した後にファイルを閉じることは必須ですが、チャンネルを閉じることは必須ではありません。
閉じたチャンネルには以下の特徴があります:
閉じたチャンネルに値を送信するとpanicが発生します。
閉じたチャンネルから受信すると、チャンネルが空になるまで値を取得し続けます。
値のない閉じたチャンネルから受信操作を行うと、対応する型のゼロ値が得られます。
すでに閉じたチャンネルを閉じるとpanicが発生します。
バッファなしのチャンネル#
バッファなしのチャンネルはブロッキングチャンネルとも呼ばれます。以下のコードを見てみましょう。
func main() {
ch := make(chan int)
ch <- 10
fmt.Println("送信成功")
}
上記のコードはコンパイルを通過しますが、実行時に以下のエラーが発生します。
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
.../src/github.com/pprof/studygo/day06/channel02/main.go:8 +0x54
なぜ deadlock エラーが発生するのでしょうか?
ch := make (chan int) で作成したのはバッファなしのチャンネルであり、バッファなしのチャンネルは誰かが値を受信するまで値を送信できません。あなたの住んでいる地域に宅配ボックスや受け取り所がないようなもので、配達員が電話をかけて、必ずその品物を手渡さなければなりません。簡単に言えば、バッファなしのチャンネルは受信がなければ送信できません。
上記のコードは ch <- 10 の行でブロックされ、デッドロックが発生します。この問題をどう解決するのでしょうか?
一つの方法は、goroutine を起動して値を受信することです。例えば:
func recv(c chan int) {
ret := <-c
fmt.Println("受信成功", ret)
}
func main() {
ch := make(chan int)
go recv(ch) // goroutineを起動してチャンネルから値を受信
ch <- 10
fmt.Println("送信成功")
}
バッファ付きのチャンネル#
上記の問題を解決するもう一つの方法は、バッファ付きのチャンネルを使用することです。
make 関数でチャンネルを初期化する際に、チャンネルの容量を指定することができます。例えば:
func main() {
ch := make(chan int, 1) // 容量1のバッファ付きチャンネルを作成
ch <- 10
fmt.Println("送信成功")
}
チャンネルの容量が 0 より大きければ、そのチャンネルはバッファ付きのチャンネルです。チャンネルの容量は、チャンネル内に格納できる要素の数を示します。あなたの地域の宅配ボックスがこれだけのスペースしかないようなもので、満杯になると入らなくなり、ブロックされます。他の人が荷物を取り出すと、配達員は一つを置くことができます。
close()#
内蔵の close () 関数を使用してチャンネルを閉じることができます(値を送信または受信しない場合は、必ずチャンネルを閉じることを忘れないでください)。
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
for i := 1; i < 5; i++ {
c <- 1
}
close(c)
}()
for {
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("main終了")
}
チャンネルから優雅に値をループして取得する方法#
チャンネルを介して有限のデータを送信する際、close 関数を使用してチャンネルを閉じることで、受信側の goroutine に値の受信を停止するよう通知できます。チャンネルが閉じられると、そのチャンネルに値を送信すると panic が発生し、そのチャンネルから受信される値は常に型のゼロ値になります。では、チャンネルが閉じられたかどうかをどうやって判断するのでしょうか?
以下の例を見てみましょう:
// チャンネルの練習
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
// goroutineを起動して0〜100の数をch1に送信
go func() {
for i := 0; i < 100; i++ {
ch1 <- i
}
close(ch1)
}()
// goroutineを起動してch1から値を受信し、その値の平方をch2に送信
go func() {
for {
i, ok := <-ch1 // チャンネルが閉じられた後に値を取得するとok=false
if !ok {
break
}
ch2 <- i * i
}
close(ch2)
}()
// メインgoroutineでch2から値を受信して印刷
for i := range ch2 { // チャンネルが閉じられるとfor rangeループを終了
fmt.Println(i)
}
}
上記の例から、値を受信する際にチャンネルが閉じられたかどうかを判断する 2 つの方法があることがわかります。通常、for range の方法を使用します。
単方向チャンネル#
時には、チャンネルをパラメータとして複数のタスク関数間で渡すことがありますが、多くの場合、異なるタスク関数でチャンネルを使用する際に制限を設けます。例えば、関数内でチャンネルを送信専用または受信専用に制限します。
Go 言語はこのような状況を処理するために単方向チャンネルを提供しています。例えば、上記の例を次のように改造します:
func counter(out chan<- int) {
for i := 0; i < 100; i++ {
out <- i
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for i := range in {
out <- i * i
}
close(out)
}
func printer(in <-chan int) {
for i := range in {
fmt.Println(i)
}
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go counter(ch1)
go squarer(ch2, ch1)
printer(ch2)
}
ここで、
chan <- int は送信専用のチャンネルで、送信は可能ですが受信はできません;
<-chan int は受信専用のチャンネルで、受信は可能ですが送信はできません。
関数の引数や任意の代入操作で双方向チャンネルを単方向チャンネルに変換することは可能ですが、その逆はできません。
一般的な例外#
注意:すでに閉じたチャンネルを閉じることも panic を引き起こします。