チャンネル#
単純に関数を同時に実行することには意味がありません。関数同士はデータを交換する必要があり、これによって同時実行の意味が生まれます。
共有メモリを使用してデータを交換することは可能ですが、異なる 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 を引き起こします。