Goの並行処理は 共有変数をチャネル上で引き回し、実行中の異なるスレッドからは同時アクセスさせない という手法を採用しています。
これを実現するために独自のプロセス/スレッド/軽量プロセス/コルーチンモデルを持っていて、このモデルを ゴルーチン(goroutine) といいます
並行処理実践の初歩段階では 正しく逐次動作するソースコードを書き、並列に動作させる改良を加える というアプローチを取った方が無難です。いきなり並行処理を書こうとするとデバッグの際に並行化の誤りなのかアルゴリズムの誤りなのかを判断するのが難しくなります。
というわけで、まず例として超単純な逐次処理を書いてみます。1から5の数値を2倍した値を表示するだけの処理です。
func main() {
params := []int{1, 2, 3, 4, 5}
for _, p := range params {
multiple(p)
}
}
func multiple(p int) {
pp := p * 2
fmt.Printf("multiple %d\n", pp)
}
当然過ぎる実行結果はこうなります。
multiple 2
multiple 4
multiple 6
multiple 8
multiple 10
では仮に、出力処理を1,2,3..と順に処理する必要が無い場合を考えてみましょう。
現状の逐次処理を分析した結果「順に処理をする=逐次処理をする 必要がない 」という判断が得られたら、それは並行化可能な処理であると言えます。
今回のケースではmultiple関数呼び出し部分が該当するので、この関数呼び出しを並行化してみましょう。
Goでは関数呼び出しを並行化(ゴルーチン化)する場合はgo <func name>
形式で呼び出します。
func main() {
params := []int{1, 2, 3, 4, 5}
for _, p := range params {
go multiple(p)
}
}
修正後の実行結果はどうなったでしょうか?何も表示されなくなったのではないでしょうか?
ゴルーチンとして関数を実行した場合、その完了を待つ事無く処理が後続に進みます。 上記の例ではmultiple関数の処理が実行される前にmain関数の処理が完了したため、何も表示されること無く終了してしまいました。
それではmultiple関数の処理を実行するためにはどうすれば良いでしょうか?ここで チャネル の使用を考えてみます。
チャネルとは、ゴルーチン実行元<=>ゴルーチン間、或いはゴルーチン同士 での値のやり取りで使用する通信チャネルです。CSPが基になっています。
チャネル型変数は<変数名> chan <やり取りする値の型>
で定義します。int型のチャネルの場合はvar ch chan int
と記述します。
実際にチャネルを使用する場合は、mapやスライスと同様にまずmake
を使用してチャネル型変数の割り当てを行います。int型のチャネルを割り当てる場合はch := make(chan int)
と記述します。
※makeの第2引数でチャネルのバッファサイズ(int型)を指定できますが、ココでは詳細は割愛しますので、詳しくは golang.jp - プログラミング言語Goの情報サイト 等でご確認下さい。
それではチャネルを使用した処理に書き換えてみます。
func main() {
params := []int{1, 2, 3, 4, 5}
ch := make(chan int) // int型のチャネルの割り当て
for _, p := range params {
go multiple(p, ch) // ゴルーチンにチャネルも渡す
fmt.Printf("multiple %d\n", <-ch) // チャネルから処理結果を受信して表示する
}
}
func multiple(p int, ch chan int) {
ch <- p * 2 // チャネルに処理結果を送信する
}
まずmultiple関数にチャネル引数を追加しました。 チャネルは"first-class value" つまり値であり、他の型の値と同じように引き回すことが可能です。
multiple関数内部ではPrintfによる出力を削除し、代わりにint引数を2倍した結果をチャネルに送信するようにしました。チャネルへの送信は<チャネル変数> <- <値/変数>
と記述します。
チャネルに送信した値は、ゴルーチン実行元で受信します。受信は送信の逆で<- <チャネル変数>
と記述します。直接変数に代入する場合は<変数> := <- <チャネル変数>
と記述します。
先に書いた "ゴルーチン呼び出し元<=>ゴルーチン間の値のやり取り" はいわば チャネルへの値の送受信 であり、「分散メモリプログラミングにおける、メッセージパッシングによるデータの共有」をチャネルという媒体を介することで実現していると言えます。
チャネルの場合、受信処理はそのチャネルへの値の送信があるまでブロックされます 。ゴルーチンを実行した場合、その完了を待つことなく後続処理が実行されると先に書きましたが、今回の改良ではチャネルからの受信処理を追加したため、そこで送信を待つことになります。結局、逐次処理と同様の挙動になったわけです。
修正後の実行結果もそうなっています。
multiple 2
multiple 4
multiple 6
multiple 8
multiple 10
並行処理を書いたとは言えこのコードでは逐次処理と同じ挙動になり、並行して動いていることを実感できないので、さらにmain関数を修正してみます。
func main() {
params := []int{1, 2, 3, 4, 5}
ch := make(chan int)
for _, p := range params {
go multiple(p, ch)
}
// チャネルからの受信を別のループで行う
for _, p := range params {
fmt.Printf("%d: multiple %d\n", p, <-ch)
}
}
1: multiple 2
2: multiple 4
3: multiple 6
4: multiple 8
5: multiple 10
入力が少ない場合はほぼ入力順に結果が表示されますが 稀に 入力順にならない事もあります。ゴルーチンが非同期で処理され順序が保証されていない為です。
ゴルーチンは処理順序が保証されない、というケースがよりハッキリする例が下記になります。multiple関数に細工をします。
func multiple(p int, ch chan int) {
if p == 2 { // 入力が2の場合、処理を1秒待つ
time.Sleep(time.Second)
}
ch <- p * 2
}
実行してみるとこんな感じで出力順序が保証されなくなりました
1: multiple 2
2: multiple 6
3: multiple 8
4: multiple 10
5: multiple 4 // 1秒待った処理の結果
以上、ザックリ過ぎる例でGoの並行処理の初歩的な部分をまとめてみました。
Goの並行処理は他にもselect case ステートメントによる "チャネルからの受信による条件分岐"や、標準パッケージ・特にsync, time, runtime 辺りのパッケージを活用した並行処理の制御など、豊富な機能が提供されていますので、そちらも確認してみてください。
※Go本家のソースをローカルに落として、grep "go△" **/*.go
すると、本家の事例が盛りだくさん