1クール続けるブログ

とりあえず1クール続けるソフトウェアエンジニアの備忘録

Goの並行処理解説ブログを参考に少しソースを書き換えてみて理解を深める

Goの並行処理について書かれたブログ

medium.com

このブログがイラスト付きで本当に可愛く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にして処理を行っていました。そのまま写経するのも面白くないのでselectcontextを使って書いてみました。

出力

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さんは神

Go1.7のcontextパッケージ | SOTA

まとめ

selectchannelの組み合わせで楽に並行処理を実装できる。場合によっては、selectじゃなくてfor~rangeのほうが良いかもしれない。 タイムアウト系はcontextパッケージを利用する。