最近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
タイムラインを見てみると以下の様になっています。
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を見てみます。
並列になりました!\(^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