コーヒーおかわり

データサイエンティストを目指す大学院生の日記。

Go言語で並列プログラミング!! ~ Go注文は並列処理ですか? ~

街の国際バリスタデータサイエンティストになりたい!

f:id:GeoTaru:20180713142945j:plain
研究室で珈琲を淹れている

Go言語とは

Go言語はGoogleのエンジニアが作った言語で、DockerやKubernetes, Vuls(脆弱性スキャナ)などに用いられている言語である。Go言語の持つ特徴の中でもっともカッコイイ特徴は並列プログラミングを楽に実装できるということだ。今日は久しぶりにGo言語を使って並列プログラミングをしよう。

題材

「コーヒーを挽いて淹れる」

といっても、コーヒーを挽いて淹れる過程を並列プログラミングでシミュレーションするだけなので、実際に作るわけではない。 手順を簡単に説明すると下記のとおり。

  • ポットでお湯を沸かす
  • コーヒー豆を挽く
  • コーヒーを淹れる

どこが並列になるかというと、「お湯を沸かしている」ところと、「コーヒー豆を挽く」ところである。お湯が沸くのをただ待つのは時間の無駄なので,お湯が沸くのを待っている間にコーヒー豆を挽く。お湯を沸騰させて、コーヒー豆をミルで砕いたら、ドリッパーの上にコーヒーフィルタをいれて、粉になったコーヒーをコーヒーフィルタに入れる。そしてお湯を注ぐ。

作成したコード

コードは下記の通り。

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("美味しいコーヒーを淹れるぞい!!")
    bw := make(chan float64) // ポットのお湯が沸いたことを通知するチャネル
    water := 500.0
    fmt.Printf("ポットに、%.1fmlの水を入れる\n", water)
    fmt.Println("ポットのスイッチをOnにする")
    go BoilWater(water, bw)
    gcb := make(chan float64) // コーヒー豆を挽いたことを通知するチャネル
    beans := 12.0
    fmt.Printf("%.1fgのコーヒー豆をコーヒーミルの中に入れて、豆を挽く\n", beans)
    go GrindCoffeeBeans(beans, gcb)
    GetReady(bw, gcb)
    // コーヒ=を淹れる
    MakeCoffee(water)
    fmt.Println("コーヒーができました")
    return
}

func BoilWater(w float64, c chan float64) {
    // お湯を沸かします
    done := time.Tick(time.Duration(w/2) * time.Second)
    for {
        select {
        case <-done:
            fmt.Println("お湯が沸いたぜよ")
            // お湯が沸いたので通知
            c <- w
            return
        default:
            fmt.Println("グツグツ!")
            time.Sleep(time.Duration(10) * time.Second)
        }
    }
    return
}

func GrindCoffeeBeans(beans float64, c chan float64) {
    estimateTime := beans * 3.0
    // コーヒーミルで豆を挽きます
    done := time.Tick(time.Duration(estimateTime) * time.Second)
    for {
        select {
        case <-done:
            // コーヒー豆を挽き終えたので通知
            c <- beans
            return
        default:
            fmt.Println("ゴリゴリ! バリバリ!")  //  コーヒーを挽く音
            time.Sleep(time.Second)
        }
    }
}

func GetReady(bwc chan float64, gcb chan float64) {
    hotWater := 0.0
    coffeeGrounds := 0.0
    for {
        select {
        case hw := <-bwc:
            hotWater += hw
            fmt.Printf("熱湯: %.1fml 用意しました\n", hotWater)
        case g := <-gcb:
            coffeeGrounds += g
            fmt.Printf("コーヒーの粉: %.1fg 用意しました\n", coffeeGrounds)
        default:
            if hotWater > 0.0 && coffeeGrounds > 0.0 {
                fmt.Println("コーヒーフィルターの端を折ってドリッパーの上に載せる")
                fmt.Println("ドリップする準備が完了")
                return
            }
        }
    }
}

func MakeCoffee(hotWater float64) {
    fmt.Println("コーヒーをドリップします")
    // 泡が出る最大量
    bubble := 1000.0
    initBubble := bubble
    check := time.Tick(20 * time.Second)
    coffee := 0.0
    for {
        select {
        case <-check:
            // 注ぐお湯の量
            pour := 100.0
            fmt.Println("もういいかい?")
            if bubble > initBubble*0.6 && hotWater > 0 {
                // お湯がまだあり、泡がまだ一定以上ある
                fmt.Println("まーだだよ")
                if hotWater >= pour {
                    hotWater -= pour
                    coffee += pour
                } else {
                    coffee += hotWater
                    hotWater = 0
                }
                // 泡が減る
                bubble -= 100.0
            } else {
                fmt.Println("もういいよ")
                time.After(20 * time.Second)
                fmt.Printf("コーヒー: %fmlできました。\n", coffee)
                return
            }
        }
    }
    return
}

実行結果

実行結果は下記の通り。

$ go run main.go
美味しいコーヒーを淹れるぞい!!
ポットに、500.0mlの水を入れる
ポットのスイッチをOnにする
12.0gのコーヒー豆をコーヒーミルの中に入れて、豆を挽く
ゴリゴリ! バリバリ!
グツグツ!
ゴリゴリ! バリバリ!
ゴリゴリ! バリバリ!
ゴリゴリ! バリバリ!
ゴリゴリ! バリバリ!
ゴリゴリ! バリバリ!
ゴリゴリ! バリバリ!
ゴリゴリ! バリバリ!
ゴリゴリ! バリバリ!
ゴリゴリ! バリバリ!
グツグツ!
ゴリゴリ! バリバリ!
ゴリゴリ! バリバリ!
~ 中略 ~
ゴリゴリ! バリバリ!
ゴリゴリ! バリバリ!
ゴリゴリ! バリバリ!
ゴリゴリ! バリバリ!
ゴリゴリ! バリバリ!
ゴリゴリ! バリバリ!
コーヒーの粉: 12.0g 用意しました
グツグツ!
グツグツ!
グツグツ!
~ 中略 ~
グツグツ!
グツグツ!
グツグツ!
グツグツ!
お湯が沸いたぜよ
熱湯: 500.0ml 用意しました
コーヒーフィルターの端を折ってドリッパーの上に載せる
ドリップする準備が完了
コーヒーをドリップします
もういいかい?
まーだだよ
もういいかい?
まーだだよ
もういいかい?
まーだだよ
もういいかい?
まーだだよ
もういいかい?
もういいよ
コーヒー: 400.000000mlできました。
コーヒーができました

解説

GrindCoffeeBeans関数でコーヒーミルでコーヒーを挽く音「ゴリゴリ!バリバリ!」を出すのと同時に、BoilWater関数でボイルでお湯を沸かす音「グツグツ!」が出ていることを確認できただろうか。BoilWater関数とGrindCoffeeBeans関数が同時に並列で動いているためこのような結果になる。

全体の流れを把握するためにmain関数に注目してほしい。

func main() {
    fmt.Println("美味しいコーヒーを淹れるぞい!!")
    bw := make(chan float64) // ポットのお湯が沸いたことを通知するチャネル
    water := 500.0
    fmt.Printf("ポットに、%.1fmlの水を入れる\n", water)
    fmt.Println("ポットのスイッチをOnにする")
    go BoilWater(water, bw)
    gcb := make(chan float64) // コーヒー豆を挽いたことを通知するチャネル
    beans := 12.0
    fmt.Printf("%.1fgのコーヒー豆をコーヒーミルの中に入れて、豆を挽く\n", beans)
    go GrindCoffeeBeans(beans, gcb)
    GetReady(bw, gcb)
    // コーヒ=を淹れる
    MakeCoffee(water)
    fmt.Println("コーヒーができました")
    return
}

"go"がBoilWater関数とGrindCoffeeBeans関数の前に書いてあることが確認できただろうか。7行目ではBoilWater関数を実行するスレッドを作成してすぐに次の8行目を実行する。つまり、BoilWater関数の終了を待つことなく次の行を実行する。 しかし、このままだとBoilWater関数の実行が終わらないうちにreturnまで実行されてプログラムが終了する。それを防ぐためにGetReady関数の中の無限ループでBoilWater関数の終了を待ち受ける。GrindCoffeeBeans関数も同様である。

func GetReady(bw chan float64, gcb chan float64) {
    hotWater := 0.0
    coffeeGrounds := 0.0
    for {
        select {
        case hw := <-bw:
            hotWater += hw
            fmt.Printf("熱湯: %.1fml 用意しました\n", hotWater)
        case g := <-gcb:
            coffeeGrounds += g
            fmt.Printf("コーヒーの粉: %.1fg 用意しました\n", coffeeGrounds)
        default:
            if hotWater > 0.0 && coffeeGrounds > 0.0 {
                fmt.Println("コーヒーフィルターの端を折ってドリッパーの上に載せる")
                fmt.Println("ドリップする準備が完了")
                return
            }
        }
    }
}

GetReady関数の無限ループについて

チャネルからの通知を待ち受ける部分についてここで説明する

    for {
        select {
        case hw := <-bwc:
            hotWater += hw
            fmt.Printf("熱湯: %.1fml 用意しました\n", hotWater)
        case g := <-gcb:
            coffeeGrounds += g
            fmt.Printf("コーヒーの粉: %.1fg 用意しました\n", coffeeGrounds)
        default:
            if hotWater > 0.0 && coffeeGrounds > 0.0 {
                fmt.Println("コーヒーフィルターの端を折ってドリッパーの上に載せる")
                fmt.Println("ドリップする準備が完了")
                return
            }
        }
    }

GetReady関数内の無限ループのなかのselect文では、caseの後に書かれたチャネルbwcやgcbから値が受信されなければ、defalut文の中の

            if hotWater > 0.0 && coffeeGrounds > 0.0 {
                fmt.Println("コーヒーフィルターの端を折ってドリッパーの上に載せる")
                fmt.Println("ドリップする準備が完了")
                return
            }

を実行し続けている。

チャネルから値を受け取った時

お湯が沸いたことを通知するためにチャネル(channel)を用いており、BoilWater関数内で

            // お湯が沸いたので通知
            c <- w

が実行されると、チャネルc(実態はチャネルbw)にwの値が入力される。
すると、GetReady関数の中のcaseが実行され、チャネルbwc(実態はチャネルbw)から変数hwに入力される。

        case hw := <-bwc:
            hotWater += hw
            fmt.Printf("熱湯: %.1fml 用意しました\n", hotWater)

チャネルの挙動や並列処理を理解できただろうか。

感想

就活前にGo言語で通信を含むプログラムを書いて以来、久しぶりにGo言語を扱った。 Go言語で並列処理を実装するのは他の言語と比べてやっぱり楽だと思う。
コーヒーを作りたくなってきたので今日はここまで。