Goの並行処理解説ブログを参考に少しソースを書き換えてみて理解を深める
Goの並行処理について書かれたブログ
このブログがイラスト付きで本当に可愛くGoの平行処理の基礎を教えてくれて良いんです。
このブログを参考に整理してみる
このブログはある一つの例を取って、並行処理に記述しています。それは鉱物の発見・掘り出し・加工です。 元のブログでは、それぞれの動作をするGopher君が描かれています(めちゃめちゃ愛らしいので是非見てください)。
シングルスレッド処理とマルチスレッド処理の違い
シングルスレッド
Gopher族Gary君が1人で鉱物の加工までをこなします。なんてマルチな才能なんだ。 ブログではシングルスレッドのソースコードは載ってなかったので愚直に書きました。
出力
From Finder: [Ore Ore Ore] From Miner: [minedOre minedOre minedOre] From Smelter: [smeltedOre smeltedOre smeltedOre]
package main import ( "fmt" "strings" ) func main() { // すべてGary君のお仕事 theMine := [5]string{"Rock", "Ore", "Ore", "Rock", "Ore"} foundOre := finder(theMine) minedOre := miner(foundOre) smelter(minedOre) } func finder(mine [5]string) []string { foundOre := make([]string, 0) for _, m := range mine { if m == "Ore" { foundOre = append(foundOre, m) } } fmt.Println("From Finder: ", foundOre) return foundOre } func miner(foundOre []string) []string { minedOre := make([]string, 0) for _, fo := range foundOre { minedOre = append(minedOre, fmt.Sprintf("mined%s", fo)) } fmt.Println("From Miner: ", minedOre) return minedOre } func smelter(minedOre []string) { smeltedOre := make([]string, 0) for _, mo := range minedOre { smeltedOre = append(smeltedOre, strings.Replace(mo, "mined", "smelted", -1)) } fmt.Println("From Smelter: ", smeltedOre) }
マルチスレッド
Gary君は鉱物の発見、Janeは掘り出し、Peterは加工を行うようにしました。
ブログのほうではそれぞれをgoroutineにして処理を行っていました。そのまま写経するのも面白くないのでselect
とcontext
を使って書いてみました。
出力
From Finder: ore From Miner: minedOre From Smelter: Ore is smelted From Finder: ore From Miner: minedOre From Smelter: Ore is smelted From Finder: ore From Miner: minedOre From Smelter: Ore is smelted
func main() { theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} oreChannel := make(chan string, 5) minedOreChan := make(chan string, 5) ctx := context.Background() ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() go func(ctx context.Context) { for { select { // Janeの仕事 case foundOre := <-oreChannel: fmt.Println("From Finder: ", foundOre) minedOreChan <- "minedOre" // Peterの仕事 case minedOre := <-minedOreChan: fmt.Println("From Miner: ", minedOre) fmt.Println("From Smelter: Ore is smelted") } } }(ctx) // Gary君の仕事 go func(mine [5]string) { for _, item := range mine { if item == "ore" { oreChannel <- item time.Sleep(10 * time.Millisecond) } } }(theMine) <-ctx.Done() }
channel
channelはgoroutineが相互にやり取りするときに使用されます。上の例だと、finderとminerがやり取りするときとminerがsmelterとやり取りするときに使用しています。goroutineはchannelに対して送信と受信ができます。<-
を使用して表現します。
oreChannel <- item // 送信 foundOre := <-oreChannel // 受信
このchannelという機構のおかげでJaneはGary君がmineをすべて見つけるのを待たずして、見つけた分から処理をしていくことができます。
channelはいろんな状況でgoroutineをブロックします。これによって同期的な処理を行うことも可能です。
バッファ付きチャネルとそうでないチャネルの2種類があります。チャネルに送信するときバッファがない場合は、ブロックされます。oreChannel := make(chan string, 5)
宣言のときにcapを指定しないとbufferなしのchannelになるはずです。
context
goの1.6まではcontextパッケージが無く自前で実装していたそうな。contextパッケージは、キャンセルを伝搬できるのも強みとのこと。 やはりdeeeetさんは神
まとめ
select
とchannel
の組み合わせで楽に並行処理を実装できる。場合によっては、select
じゃなくてfor~range
のほうが良いかもしれない。
タイムアウト系はcontextパッケージを利用する。