Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
74
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

GAE/Goでもgoroutine使おうぜ!というハナシ

最近Goがすっかりメジャーになってきて自分の書けるネタもなくなってきたので今年もやっぱりGAE/Goのハナシです。
GAE(Google App Engine)もGoに負けずもっと盛り上がって欲しいですね(^^;

前置き

さて、今回のテーマですが・・

GAE/Goでもgoroutine使おうよ!

てハナシです。

GAEの環境は残念ながらGOMAXPROCSが1になっている為1「goroutineを使っても意味がない」と考えている方もいらっしゃるかと思います。

実はうまく使えばとても強力なのです!

どういうことかというと、あるgoroutineがAPI呼び出しなどでI/O待ちに入ると、他のgoroutineにスケジュールされます。
つまり、複数のAPI呼び出しをgoroutineを使うことで実質並列化することが可能なのです。

GAEは基本的にたくさんの用意されているサービス(DatastoreやTaskQueueなど)をAPIで呼び出してアプリケーションを作っていきますので、goroutineを使うことでリクエストのレイテンシを下げることが期待出来ます。

そもそもGAE/JavaやGAE/pythonの各種APIには同期版とセットで非同期版が用意されており並列実行が可能となっていますが、GAE/GoのAPIには非同期版がありません。
Go言語自体にgoroutineがあるんだから、それ使えばいいじゃん!というメッセージなのだと思っています(多分)。

Java、Pythonの非同期版APIではAPI単位でしか非同期に出来ませんが、goroutineの場合は任意のブロックで非同期に出来るので、例えば異なるサービスのAPI呼び出しを纏めて並列に実行することも容易にできます。2

また、goroutineの使い方さえマスターしてしまえばいちいちAPIリファレンスを参照しなくても非同期処理を書ける、というメリットもあります。

goroutine使用例

GAE/Goでのgoroutineの使用例をお見せします。

一番よく使う例として、Datastoreのアクセスを見てみましょう。

Hoge, Fuga, Piyoの3つのKind(RDBでいうところのテーブル)からそれぞれエンティティ(RDBでいうところのレコード)をGetします。

↓エンティティ格納用構造体を定義。

type Hoge struct {
    Name string
}

type Fuga struct {
    Name string
}

type Piyo struct {
    Name string
}

まずはgoroutineなしバージョン

    ctx := appengine.NewContext(r)

    var hoge Hoge
    hogeKey := datastore.NewKey(ctx, "Hoge", "hoge", 0, nil)
    if err := datastore.Get(ctx, hogeKey, &hoge); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    var fuga Fuga
    fugaKey := datastore.NewKey(ctx, "Fuga", "fuga", 0, nil)
    if err := datastore.Get(ctx, fugaKey, &fuga); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    var piyo Piyo
    piyoKey := datastore.NewKey(ctx, "Piyo", "piyo", 0, nil)
    if err := datastore.Get(ctx, piyoKey, &piyo); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

こんな感じですね。
GAEにデプロイして実行します。

何回か実行すると、App EngineダッシュボードからTraceを見ることが出来るようになります(管理コンソールメニュー[Traces]からも辿れます)。3

cloudtrace.png

タイムラインを見てみると以下の様になっています。

timeline.png

3つのdatastore API呼び出しがシーケンシャルに実行されています(想定通り)。

では、goroutineを使って非同期呼び出しにしてみましょう。

goroutineの利用パターンいろいろあるかと思いますが、私はchannelをfutureっぽく使うのを好んでいます。

↓Hogeの取得

    type hogeOrErr struct {
        hoge *Hoge
        err  error
    }

    futureHoge := make(chan hogeOrErr)
    go func() {
        var hoge Hoge
        hogeKey := datastore.NewKey(ctx, "Hoge", "hoge", 0, nil)
        if err := datastore.Get(ctx, hogeKey, &hoge); err != nil {
            futureHoge <- hogeOrErr{nil, err}
            return
        }
        futureHoge <- hogeOrErr{&hoge, nil}
    }()

同期版の処理をまるっとgoroutineで囲んで、結果はchannelに返してます。結果を取得する処理なのでstruct作っていますがerrorしか戻さない処理ならばシンプルにchan errorでOKです。

Fuga、Piyoも同様です(割愛)。

まず並列に実行可能なAPIを全て非同期実行した後、結果の同期を取ります。

    var errs []error

    // Hoge取得
    hogeResult := <-futureHoge
    if hogeResult.err != nil {
        errs = append(errs, hogeResult.err)
    }

    // Fuga取得
    fugaResult := <-futureFuga
    if fugaResult.err != nil {
        errs = append(errs, fugaResult.err)
    }

    // Piyo取得
    piyoResult := <-futurePiyo
    if piyoResult.err != nil {
        errs = append(errs, piyoResult.err)
    }

    // エラーチェック
    if len(errs) > 0 {
        return nil, appengine.MultiError(errs)
    }

それぞれのchannelから取得するタイミングで処理がブロックされます。
ブロックされている間も各APIは処理を行っているので、結果全体のレイテンシは一番処理時間の長いgoroutineに収束します。

デプロイ、実行してTraceを見てみます。

getasync2.png

並列になりました!\(^o^)/
処理時間も1/3以下になってます。

上記例ではchannelで同期取ってますが、MutexやWaitGroup使っても構いません。その辺は好みで。
私も複雑な処理にgoroutine組み合わせるときはMutexで同期を取ったMapを受け渡してそこに結果を格納したりしています。

参考: https://github.com/knightso/base/blob/master/gae/ds/map.go

注意点

goroutineは他言語のthreadと比べてとても軽量ですが、それでも生成毎に数KBのstackを消費していきますので、巨大なループや深い再帰処理等で大量に生成する場合は注意しましょう。4

datastoreのGetMulti、PutMultiなどAPIレベルで複数件同時に扱う手段がある場合は、そちらを使いましょう。GAEの外側で並列実行してくれるので。

おまけ

昔同じテーマで発表したときに書いた簡易クローラサンプル

同期版
https://github.com/hogedigo/gaegoisnice/blob/master/webcrawler/crawler/crawler.go

非同期版
https://github.com/hogedigo/gaegoisnice/blob/master/webcrawler/crawler/crawlerp.go


  1. MVMsを使うとその限りではないがそれはまた別のお話 

  2. Javaでthread使っても同様のことができますが、goroutineに比べてかなり重い上に最大50という制限があります 

  3. 以前はappstatsというツールをアプリに組み込まないと見れなかった情報です。現在はappstatsなしで見られる様になりました。便利!(^_^) 

  4. 試しにF1インスタンスでシンプルなgoroutineを50000個生成したらエラーになりました 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
74
Help us understand the problem. What are the problem?