2019年4月29日月曜日

golang の goroutine がどれくらい効果が簡単に検証してみた

概要

goroutine は golang で簡単に使える並列化の仕組みです
どんな場面で効果がありそうなのか実際に試してみました

環境

  • macOS 10.14.4
  • golang 1.11.5

goroutine なし

ある外部サイトに連続してアクセスするような処理を考えます
またそれぞれのアクセスは疎結合になっておりレスポンスに応じて次の処理に進むといったことはないものとします
例えば 10 回連続でアクセスする場合に単純に直列で処理する場合には以下のようになると思います

package main

import (
    "fmt"
    "net/http"
)

func call(c int) {
    for i := 0; i < c; i++ {
        resp, err := http.Get("http://localhost:9292/")
        if err != nil {
            fmt.Println(err)
            return
        }
        fmt.Println(resp.StatusCode)
    }
}

func main() {
    for i := 0; i < 10; i++ {
        call(1)
    }
}

これを自分の環境で何度か実行してみるとだいたい 10 - 20 秒の範囲で完了しました

real    0m16.395s
user    0m0.101s
sys     0m0.045s

これを goroutine で並列化するとどれくらい速度が上がるかを考えます

goroutine あり (5多重)

10 回を 5 多重なのでそれぞれ 2 回づつ外部サイトにアクセスします
goroutine 版に書き換えると以下のようになります

package main

import (
    "fmt"
    "net/http"
    "sync"
)

func call(c int) {
    for i := 0; i < c; i++ {
        resp, err := http.Get("http://localhost:9292/")
        if err != nil {
            fmt.Println(err)
            return
        }
        fmt.Println(resp.StatusCode)
    }
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(c int) {
            call(2)
            defer wg.Done()
        }(2)
    }
    wg.Wait()
}

これを自分の環境で実行してみるとだいたい 2 - 4 秒くらいの間で終了するようになりました
比率で言うと 5 倍ほど早くなっています
単純に並列化した分だけ速度が速くなっていると言えると思います

real    0m2.986s
user    0m0.096s
sys     0m0.037s

sync.waitGroup を使ってそれぞれの goroutine が終了するのを待つようになっています
なので例えばどれか 1 つの goroutine だけレスポンスが低下するとそれに引きづられて全体の速度も下がるという懸念はあります

ちょっと考察

今回のサンプルのように並列化する処理同士が干渉し合わないような場合には goroutine はかなり効果的だと思います
また sync.waitGroup も使わずに完全に goroutine に任せられる場合には更に速度の向上が見込めると思います

当然ですが、goroutine はメインのプロセスが終了すると goroutine が終了しているかどうかに関わらず強制終了していまいます
今回はメイン側のプロセスを停止させないために sync.waitGroup を使っていますがそもそもメインプロセスがデーモンプロセスになっているのであれば不要な場合もあります

処理を並列化してその後の処理が goroutine の処理の終了に関わらずできるのであれば投げてそのまま次に進んでもいいですが、そうでない場合は sync.waitGroup などの仕組みを使って待つ必要が出てきます
それは仕方ないというかシステムの都合にもよるかなと思います

そもそも goroutine を使わない

goroutine は標準のパッケージだけを使って簡単に並列化できるのが一番のメリットかなと思います
ただ今回のケースのように処理がお互いに疎結合になっていたり結果に依存していない場合には向いていますがそうでない場合にはいろいろと考慮が必要です
目的や使っている技術や環境にもよりますが単純に並列化したいのであれば go-workers を使うのも手かなと思います
goroutine とは違い外部のパッケージを使うのと Redis も使います
ただサブプロセスではなくなるのでメイン側のプロセスが死んでもサブプロセス (ワーカー) は死にません
ワーカープロセスを単純に増やせば多重度も簡単に上げられます
またメンテナンスもしやすくなると思います

といった具合に状況に応じては goroutine で並列化しないで別の仕組み (パッケージなど) を使ったほうがうまく合致する場合があることを頭に入れておくと良いと思います
何でもかんでも goroutine ではなく状況に応じて別の方法を検討したほうが良いということです

それをどう判断すればいいのか

じゃあ goroutine を使うかどうかをどう判断すればいいかですがこれは正直経験則しかないかなと思っています
もちろん事前にどちらも効果測定したりテストしたりして感触を確認することはできますが、その場合は 2 通り実装しなければいけません
設計の段階でどの方法の並列化がベストなのか間違いなく判断できるのは、そっいった経験を積んだプログラマでないと難しいかもしれません、、

最後に

goroutine を使うと実際に処理が速くなるのを確認してみました
また状況に応じては goroutine よりも別の仕組みを使ったほうが良さそうだということも検討してみました
自分はそこまで golang に詳しいわけではないので参考程度に見てもらえると助かります

0 件のコメント:

コメントを投稿