The Go gopher was designed by Renée French.
The gopher stickers was made by Takuya Ueda.
Licensed under the Creative Commons 3.0 Attributions license.
GAE/Goとsyncパッケージ
2018年3月2日(金)
@酔いどれGCPUG
自己紹介
上田拓也
@tenntenn
所属
コミュニティ活動
&
Go ビギナーズ
Go Conference
上田拓也
@tenntenn
ソウゾウ エキスパートチーム
技術をアウトプットするところに技術は集まる
■ エキスパートチームとは?
● 50%以上の時間を技術コミュニティへの貢献に充てる
■ エキスパートチームの役割
● 社内に新しい技術を取り取り込む
● 社外のコミュニティなどを通じて社会へ還元する
■ エキスパートチームの活動
● カンファレンス・勉強会の開催/運営
● 対外的な講演活動
● 執筆、雑誌への寄稿、インタビュー
● 社内外での担当技術の普及推進
@tenntenn
担当:Go・GCP
@mhidaka
担当:Android
メンバー
アジェンダ
■ ゴールーチンとチャネル
■ GAE/Goでのゴールーチンの使い所
■ syncパッケージ
■ golang.org/x/syncパッケージ
■ github.com/tenntenn/syncパッケージ
■ まとめ
Goの並行処理
■ ゴールーチン
■ 軽量なスレッドに近いもの
■ goキーワードをつけて関数呼び出し
■ チャネル
■ ゴールーチン間のデータのやり取り
■ 安全にデータをやり取りできる
5
チャネル
ゴールーチン
A
ゴールーチン
B
データ
データ
// 関数fを別のゴールーチンで呼び出す
go f()
GAE/Goでの並行処理の使い所
■ 並列度は1
● GAE/Go上ではゴールーチンは並列には実行されない
● ずっと1とは限らないので適切にロックは取ろう
■ ゴールーチンのスケジューラの挙動を把握する
● 特定の処理が走ると別のゴールーチンに処理が切り替わる
○ I/Oやシステムコール、チャネルのブロックなど
● Datastoreへ複数のリクエストを投げる場合には有効
○ 通信が発生しているため
Cloud
Datastore
App
Engine
Get A
Get B
Get C
ゴールーチン間のデータ競合
ゴールーチン-main
ゴールーチン-2ゴールーチン-1
go f1() go f2()
変数v
print(v) v = 100
処理順序が保証されない
競合
7
データ競合の解決
■ 問題点
■ どのゴールーチンが先にアクセスするか分からない
■ 値の変更や参照が競合する
■ 解決方法
■ 1つの変数には1つのゴールーチンからアクセスする
■ チャネルを使ってゴールーチン間で通信をする
■ またはロックをとる(syncパッケージ)
Do not communicate by sharing memory;
instead, share memory by communicating
8
syncパッケージ
■ チャネル以外を使う理由
● チャネルだけを使っているとコードが難解になる場合がある
● 複数のチャネルが登場したり
● 競合を防ぎたいデータが複数ある場合
■ syncパッケージ
● データの競合を防ぐロックなどを提供するパッケージ
● sync/atomicではアトミックな演算をするための型などを提供
9
ロック
var m sync.Mutex
m.Lock()
go func() {
time.Sleep(3 * time.Second)
m.Unlock()
fmt.Println("unlock 1")
}()
m.Lock()
m.Unlock()
fmt.Println("unlock 2")
Playgroundで動かす
ゼロ値で使える
ここでブロック
10
■ sync.Mutex
● Lockメソッドを呼ぶとUnlockメソッドが呼ばれるまで
Lockメソッドの呼び出しでブロックする
書き込み・読み込みロック
■ sync.RWMutex
● Mutexに読み込み用のRLockとRUnlockが入ったもの
var m sync.RWMutex
m.RLock()
go func() {
time.Sleep(3 * time.Second)
m.RUnlock()
fmt.Println("unlock 1")
}()
m.RLock()
m.RUnlock()
fmt.Println("unlock 2")
Playgroundで動かす
読み込みロックだけでは
ブロックしない
11
1度しか実行しない関数
■ sync.Onceを使う
● 1回以上Doメソッドを呼んでも意味がない
● 複数のゴールーチンから1回しか呼ばないようにするために利用する
func f() { fmt.Println("Do!!") }
func main() {
var once sync.Once
once.Do(f)
once.Do(f)
fmt.Println("done")
}
12
2回目は実行されない
Playgroundで動かす
sync.Onceを使って初期化を行う
■ init関数を用いる
● context.Contextを取得できないのでログすらだせない
■ /_ah/warmupリクエストで処理する
● インスタンス起動時に送られてくるリクエスト
● app.yamlで設定して、ハンドラを設定すればよい
● しかし、必ず最初にリクエストがくるわけではない
○ sync.Onceで処理して必ず1回だけ実行されるようにする
inbound_services:
- warmup
warmup
first requst
初期化
処理
sync.Once
1回だけ実行
複数のゴールーチンの待機
■ sync.WaitGroupを使う
var wg sync.WaitGroup
wg.Add(1)
go func() { /* do something */ wg.Done() }()
wg.Add(1)
go func() { /* do something */ wg.Done() }()
wg.Wait()
Playgroundで動かす
14
エラーを返すゴールーチンの待機
■ golang.org/x/sync/errgroupを使う
● 失敗した場合にエラーが取得できる
● 1つでもエラーを起こすとキャンセルされる
var eg errgroup.Group
eg.Go(func() error { /*...*/ })
eg.Go(func() error { /*...*/ })
if err := eg.Wait(); err != nil {
log.Fatal(err)
}
15
golang.org/x/syncパッケージ
■ errgroupパッケージ
● エラーを返すゴールーチンの待機
■ singleflightパッケージ
● 1度しかリクエストが飛ばないようにする
● 2度目は同じ値を返す
■ semaphoreパッケージ
● 重み付きセマフォ
■ syncmapパッケージ
● スレッドセーフなマップ
● Go1.9から標準パッケージになった
github.com/tenntenn/syncパッケージ
■ recoverableパッケージ
● ゴールーチンまたいだpanicを安全に処理する
■ fcfsパッケージ
● 早い物勝ち(First Come First Serve)
● 最初に処理したものだけ有効にする
■ groutinegroupパッケージ
● x/errgroupパッケージのエラーが発生しても全部続ける版
● まだ非公開
● メルカリ アッテで使われている
recoverableパッケージ
■ ゴールーチンを跨いでpanicをrecoverできない
■ panicをエラーに変換する
func main() {
defer func() { fmt.Println(recover()) }()
go func() { panic("PANIC!!!") }()
time.Sleep(1 * time.Second)
}
func main() {
var eg errgroup.Group
eg.Go(recoverable.Func(func() { panic("PANIC!!!") }))
if err := eg.Wait(); err != nil { /* エラー処理 */ }
}
fcfsパッケージ
■ 早い者勝ち(First Come First Served)
● とあるサービスからリソースを取得することを考える
● 1回目のリクエストを投げる
● 1回目が一定期間で帰ってこなかったらもう一つ投げる
● 早く帰ってきた方を使って、他方をキャンセルする
リクエスト
リクエスト
100ms待機
fcfsパッケージ
■ 早い者勝ち(First Come First Served)
● とあるサービスからリソースを取得することを考える
● 1回目のリクエストを投げる
● 1回目が一定期間で帰ってこなかったらもう一つ投げる
● 早く帰ってきた方を使って、他方をキャンセルする
var g fcfs.Group
f := func() (interface{}, error) { /* 処理 */ }
g.Go(f)
g.Delay(100 * time.Millisecond, f) // 100ms遅れて処理
v, err := g.Wait()
if err != nil { /* エラー処理 */}
/* vを使った処理 */
goroutinegroupパッケージ
■ エラーと処理結果をそれぞれ取り出せる
● エラーが発生しても他の処理には影響しない
● エラーと結果が安全に取り出せる
var g goroutinegroup.Group
g.Go("処理1", func() (interface{}, error) { /* 処理1 */ })
g.Go("処理2", func() (interface{}, error) { /* 処理1 */ })
<-g.Done()
v1, err := g.Return("処理1")
if err != nil { /* エラー処理 */}
v2, err := g.Return("処理2")
if err != nil { /* エラー処理 */}
/* v1とv2を使った処理 */
まとめ
■ GAE/Goでゴールーチンは有効
● 現在の並列度は1
○ ずっと1とは限らないので注意
● 複数のDatastoreへのアクセスなどを並行に投げれる
■ syncパッケージを使おう
● 初期化処理はsync.Onceで行う
● Group系の型を使えば楽にゴールーチンを利用できる
Thank you!
twitter: @tenntenn
Qiita: tenntenn
connpass: tenntenn
23

GAE/Goとsyncパッケージ