バッファーありのチャネルの概要
説明したように、既定でチャンネルはバッファーされません。 つまり、"受信" 操作がある場合にのみ、"送信" 操作が受け付けられます。 それ以外の場合、プログラムは、無期限に待機し、ブロックされます。
goroutine 間でそのような種類の同期が必要になる場合があります。 ただし、単にコンカレンシーを実装する必要があり、goroutine が相互に通信する方法を制限する必要はない場合もあります。
バッファーありのチャネルはキューのように動作するため、バッファーありのチャネルの場合、プログラムがブロックされずにデータが送受信されます。 次のように、チャネルを作成するときにこのキューのサイズを制限できます。
ch := make(chan string, 10)
チャネルに何かを送信するたびに、要素がキューに追加されます。 次に、受信操作によって要素がキューから削除されます。 チャネルがいっぱいになると、データを保持する領域ができるまで、送信操作は待機されます。 逆に、チャネルが空で読み取り操作がある場合、読み取る対象が発生するまでチャネルはブロックされます。
バッファーありのチャネルを理解するための簡単な例を次に示します。
package main
import (
"fmt"
)
func send(ch chan string, message string) {
ch <- message
}
func main() {
size := 4
ch := make(chan string, size)
send(ch, "one")
send(ch, "two")
send(ch, "three")
send(ch, "four")
fmt.Println("All data sent to the channel ...")
for i := 0; i < size; i++ {
fmt.Println(<-ch)
}
fmt.Println("Done!")
}
このプログラムを実行すると、次の出力が表示されます。
All data sent to the channel ...
one
two
three
four
Done!
実行内容は何も変わっていないではないか、と言われるかもしれませんが、そのとおりです。 ただし、次のように、size
変数を小さい数値に変更するとどうなるか見てみましょう (大きい数値で試すこともできます)。
size := 2
このプログラムを再実行すると、次のエラーを受け取ります。
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.send(...)
/Users/developer/go/src/concurrency/main.go:8
main.main()
/Users/developer/go/src/concurrency/main.go:16 +0xf3
exit status 2
その理由は、send
関数の呼び出しがシーケンシャルであるためです。 新しい goroutine を作成しているのではありません。 そのため、キューに格納されるものはありません。
チャネルは、goroutine と緊密に接続されています。 チャネルからデータを受信する別の goroutine がないと、プログラム全体が無限にブロック状態になる可能性があります。 ご覧のとおり、このようなことは起こります。
それでは、何かおもしろいものを作ってみましょう。 最後の 2 つの呼び出し (最初の 2 つの呼び出しはバッファーに適切に収まります) について goroutine を作成し、for ループを 4 回実行します。 コードは次のとおりです。
func main() {
size := 2
ch := make(chan string, size)
send(ch, "one")
send(ch, "two")
go send(ch, "three")
go send(ch, "four")
fmt.Println("All data sent to the channel ...")
for i := 0; i < 4; i++ {
fmt.Println(<-ch)
}
fmt.Println("Done!")
}
このプログラムを実行すると、期待どおりに動作します。 チャネルを使用するときは、常に goroutine を使用することをお勧めします。
必要以上の要素でバッファー付きチャネルを作成するケースをテストしましょう。 前に使用した例を使用して API をチェックし、バッファー付きチャネルをサイズ 10 で作成します。
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
start := time.Now()
apis := []string{
"https://management.azure.com",
"https://dev.azure.com",
"https://api.github.com",
"https://outlook.office.com/",
"https://api.somewhereintheinternet.com/",
"https://graph.microsoft.com",
}
ch := make(chan string, 10)
for _, api := range apis {
go checkAPI(api, ch)
}
for i := 0; i < len(apis); i++ {
fmt.Print(<-ch)
}
elapsed := time.Since(start)
fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}
func checkAPI(api string, ch chan string) {
_, err := http.Get(api)
if err != nil {
ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
return
}
ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}
このプログラムを実行すると、前と同じ出力が表示されます。 チャネルのサイズを小さい数値や大きい数値に変更してみても、プログラムは機能します。
バッファーなしとバッファーありのチャネル
この時点で、別の種類を使用するのはいつなのか、と思われるかもしれません。 それは、goroutine 間で通信をどのように流したいかによって決まります。 バッファーなしのチャネルの場合、同期的に通信されます。 データを送信するたびに、誰かがチャネルから読み取るまでプログラムがブロックされることが保証されます。
逆に、バッファーありのチャネルの場合、送信操作と受信操作は分離されています。 プログラムはブロックされませんが、(前述のように) デッドロックが発生する可能性があるので注意する必要があります。 バッファーなしのチャネルを使用する場合、同時に実行できる goroutine の数を制御できます。 たとえば、API を呼び出し、毎秒実行する呼び出しの数を制御したい場合があります。 そうしないと、ブロックされる可能性があります。
チャネルの方向
Go のチャネルには、興味深い特徴がもう 1 つあります。 関数のパラメーターとしてチャネルを使用するとき、チャネルの目的としてデータの "送信" または "受信" を指定できます。 プログラムが大きくなるにつれて、関数の数が多くなりすぎることがあります。適切に使用できるように、各チャネルの意図を文書化しておくことをお勧めします。 または、ライブラリを作成していて、データの整合性を維持するためにチャネルを読み取り専用として公開する場合もあります。
チャネルの方向を定義するには、データを読み取るときまたは受信するときと同様の方法で行います。 ただし、関数パラメーターでチャネルを宣言するときに行います。 チャネルの型を関数のパラメーターとして定義する構文は、次のようになります。
chan<- int // it's a channel to only send data
<-chan int // it's a channel to only receive data
receive-only にする想定のチャネルを介してデータを送信すると、プログラムのコンパイル時にエラーが発生します。
次のプログラムを 2 つの関数の例として使用してみましょう。1 つはデータを読み取り、もう 1 つはデータを送信します。
package main
import "fmt"
func send(ch chan<- string, message string) {
fmt.Printf("Sending: %#v\n", message)
ch <- message
}
func read(ch <-chan string) {
fmt.Printf("Receiving: %#v\n", <-ch)
}
func main() {
ch := make(chan string, 1)
send(ch, "Hello World!")
read(ch)
}
このプログラムを実行すると、次の出力が表示されます。
Sending: "Hello World!"
Receiving: "Hello World!"
このプログラムでは、各関数における各チャネルの意図が明確にされています。 チャネルを使用して、データの受信のみを目的とするチャネルでデータを送信しようとすると、コンパイル エラーが発生します。 たとえば、次のようなことを試します。
func read(ch <-chan string) {
fmt.Printf("Receiving: %#v\n", <-ch)
ch <- "Bye!"
}
このプログラムを実行すると、次のエラーが表示されます。
# command-line-arguments
./main.go:12:5: invalid operation: ch <- "Bye!" (send to receive-only type <-chan string)
チャネルを誤用するよりも、コンパイル エラーが発生する方がましです。
多重化
最後に、select
キーワードを使用して複数のチャネルを同時に操作する方法を確認しましょう。 複数のチャネルで作業しているときに、イベントが発生するまで待機したい場合があります。 たとえば、プログラムが処理しているデータに異常がある場合に操作を取り消すロジックを含めることができます。
select
ステートメントは switch
ステートメントと同じように機能しますが、チャネル用です。 処理対象のイベントを受信するまで、プログラムの実行はブロックされます。 複数のイベントを取得した場合、ランダムに 1 つが選択されます。
select
ステートメントの重要な側面は、イベントを処理した後に実行が終了されることです。 さらにイベントが発生するまで待機したい場合は、必要に応じてループを使用します。
次のプログラムを使用して、select
の動作を確認してみましょう。
package main
import (
"fmt"
"time"
)
func process(ch chan string) {
time.Sleep(3 * time.Second)
ch <- "Done processing!"
}
func replicate(ch chan string) {
time.Sleep(1 * time.Second)
ch <- "Done replicating!"
}
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go process(ch1)
go replicate(ch2)
for i := 0; i < 2; i++ {
select {
case process := <-ch1:
fmt.Println(process)
case replicate := <-ch2:
fmt.Println(replicate)
}
}
}
このプログラムを実行すると、次の出力が表示されます。
Done replicating!
Done processing!
replicate
関数が最初に完了しました。そのため、その出力が最初にターミナルに表示されます。 select
ステートメントはイベントを受信するとすぐに終了するため、main 関数にはループがありますが、process
関数が完了するまでまだ待機中です。