[Go言語]Channelを使い倒そうぜ!

こんにちは。KOUKIです。

とあるWeb系企業でシステム開発をしています。Go言語を使ったプロジェクトに参加することが多いので割とGo言語に明るくなってきました。

最近では、goroutineを使った並行処理の実装も増えて来ており、日々スキルアップを実感します。

そんな中、Go言語の同期処理でよく使うChannelが結構便利だと感じたので、その使い方をまとめておこうと思います。

Channelとは

Channelは、メモリアクセス同期に使える、Go言語の同期処理のプリミティブの一つです。

メモリに対するアクセスに使える一方で、ゴルーチン間の通信に使うのが最適です。

例をあげてみましょう。

Channelを使用するには、最初にmake(chan datatype)で宣言する必要があります。以下の方法でもOKです。

Channelへの値の格納/取り出しには「<-」アロー演算子を使えばOKです。

格納するときは「ch<-1」、取り出すときは「<-ch」です。

このチャネルを読み込もうとすると必ず同じメモリを参照するので、プログラムのどこか他の場所でその値を読み込ませることが可能です。

そして何より面白いのは、Channelはgoroutineをブロックします!

理由を説明する前に、このプログラムを一度実行します。

無事に値を取り出せました。

goroutineが起動するとGo言語のランタイムに登録されますが、それがいつ実行されるかわかりません。つまり、無名関数で実行したゴルーチンより先にメインの処理が終了してしまう可能性があります(無名関数は永久に実行されない)。

しかし、Channelはゴルーチンをブロックするので、確実に処理が実行されます。

一方向Channel

Channelは、「<-」演算子を使うことで、Channelの向きを表現し、一方向だけにデータが流れるように宣言することが可能です。

つまり、送信だけ、受信だけのChannelを作れるということです。

ちなみに向きに反した実装を行うと怒られます。

Channelのブロック

もう一度以下の例をみてください。

この例では、メインゴルーチンの中で無名関数のgoroutineを作成しています。

プログラムを実行すると次の出力が確認できます。

「Writing」が出力される前に処理が終了しない理由は、前述の通り、Channelがメインゴルーチンをブロックしているからです。

つまり、以下のことが言えます。

  • キャパシティが一杯のチャネルに書き込もうとするゴルーチンは、チャネルの空きが出るまで待機する
  • 空のチャネルから読み込もうとするゴルーチンは、チャネルに要素が入ってくるまで待機する

では、チャネルへの書き込みを一旦、コメントアウトしてみましょう。

このコードを実行すると以下のdeadlockが発生します。

これは、「fmt.Println(<-readCh)」にて、チャネルから値を読み込もうと永遠に待ち続けるため、発生する例外です。

KOUKI
KOUKI

チャネルは簡単に実装できるけど、制御はとても難しい

Channelの戻り値

Channelは、オプションとして2つの値を返すことができます。

2つ目の値としてTrueが帰ってきましたね。

これは、以下の値を示します。

  • プロセス内のどこかで書き込みがあったことで、読み込み演算子が読み込みができたかどうか
  • 閉じたチャネルから生成されたデフォルトの値

閉じたチャネルのデフォルトの値は、チャネルを閉じた時に取得できます。

0はint型デフォルト値ですね。String型はスペース、bool型はfalseが入ります。

面白いのが、閉じたチャネルの読み込みが行えることです。

これであるチャネルに関して、上流の書き込みに対し、複数の下流で読み込みが行えますし、条件判定も行えます。

Channelのループ処理

Channelは、ループ処理が可能です。for … rangeを使います。

プログラムを実行してみます。

1~5までの数値が出力されましたね。for … rangeにてChannelを繰り返し読み込んでいます。そして、Channelがクローズされた時、このループが終了します

では、「defer close(ch)」をコメントアウトしたらどうなるでしょうか?

答えは、デッドロックが発生します。

これはChannelが閉じられないため、ループが完了せずに、永久にChannelを読み込み続けるからです。

Channelとシグナル

Channelを閉じると複数のゴルーチンに同時にシグナルを送ることができます。

上記のコードでは、loop関数を5つ、ゴルーチン化しました。そして、「<-ch」にてgoroutineをブロックしています。

このブロックは、「close(ch)」を実行した時に解放されます。

バッファつきChannel

Channelには、バッファをつけることができます。

このバッファが一杯になった場合、このチャネルへの書き込みはブロックされます。

この性質を利用すれば、Goroutineの最大起動数を制御することもできます。

上記のコードでは、バッファサイズ4のChannelを用意しました。つまり、goroutineの最大起動数は、4つになるようにしています。

fmt.Printf(“The Number of goroutine: %d\n”, len(ch))」にて、Channel数を出力できるようにしているので、この数が5以上にならなかったら成功です。

OKですね。

Channelの初期化の重要性

Channelは、makeを使って初期化をしますが、初期化をしなかった場合はどうなるでしょうか。

読み込みの場合

最初に読み込みの場合をみてみましょう。

初期化していない場合は、デッドロックが走りますね。

書き込みの場合

次は、書き込みの場合です。

書き込みの場合でも、デッドロックが走りました。

closeした場合

最後にcloseした場合です。

初期化なしでcloseした場合は、デッドロックではなく、panicが発生するようですね。

この結果から、Channelを扱う前には、確実に初期化をする必要があります。

おわりに

Channelはうっかりすると、プログラムそのものを永久にブロックしてしまう可能性があるなど、取扱注意な機能です。

しかし、goroutineをまとめる糊のような物でもあるので、使いこなしていきたいですね。

それでは、また!

関連記事

参考書籍