Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Speaker Deck
PRO
Sign in
Sign up for free
Go と並行処理
RyotaNakaya
December 03, 2021
Technology
0
97
Go と並行処理
RyotaNakaya
December 03, 2021
Tweet
Share
More Decks by RyotaNakaya
See All by RyotaNakaya
エンジニアと要件定義
ryotanakaya
0
110
ワクワク!Rubyクイズ!!
ryotanakaya
0
670
増え続けるトランザクションデータと向き合う
ryotanakaya
0
130
シャッフルランチシステムを刷新してみた話
ryotanakaya
0
73
Other Decks in Technology
See All in Technology
日経電子版だけじゃない! 日経の新規Webメディアの開発 - NIKKEI Tech Talk #3
sztm
0
320
立ち止まっても、寄り道しても / even if I stop, even if I take a detour
katoaz
0
700
【NGK2023S】 ノードエディタ形式の画像処理ツール「Image-Processing-Node-Editor」
kazuhitotakahashi
0
300
SmartHRからOktaへのSCIM連携で作り出すHRドリブンのアカウント管理
jousysmiler
1
120
PCI DSS に準拠したシステム開発
yutadayo
0
310
OpenShiftのリリースノートを整理してみた
loftkun
2
400
【Λ(らむだ)】WinActorから始めるいつのまにリスキリング / WinAtorライトニングトーク大会20230123
lambda
0
120
💰年度末予算消化祭💰 Large Memory Instance で 画像分類してみた
__allllllllez__
0
100
Cloudflare Workersで動くOG画像生成器
aiji42
1
490
金属加工屋の営業マンがSTマイクロで・・・
usashirou
0
170
Deep dive in Reserved Instance ~脳死推奨量購入からの脱却~
kzkmaeda
0
540
KyvernoとRed Hat ACMを用いたマルチクラスターの一元的なポリシー制御
ry
0
190
Featured
See All Featured
JavaScript: Past, Present, and Future - NDC Porto 2020
reverentgeek
38
3.6k
GraphQLとの向き合い方2022年版
quramy
20
9.9k
Teambox: Starting and Learning
jrom
124
7.9k
How STYLIGHT went responsive
nonsquared
89
4.2k
The Cult of Friendly URLs
andyhume
69
5.1k
Unsuck your backbone
ammeep
659
56k
Atom: Resistance is Futile
akmur
256
24k
Intergalactic Javascript Robots from Outer Space
tanoku
261
26k
Typedesign – Prime Four
hannesfritz
34
1.5k
Building Adaptive Systems
keathley
27
1.3k
Build your cross-platform service in a week with App Engine
jlugia
221
17k
The Art of Programming - Codeland 2020
erikaheidi
36
11k
Transcript
2021/12/03 Nakaya Ryota Goと並行処理
ギフティ入社:2019年1月 所属:ContentsCreation Div. ProductUnit2(コーヒー屋さんチーム) 前職:バックオフィス系システムのパッケージベンダー(上流メイン) 社内部活:#coffee、#darts、#poker、#among_us… 分報:#times_nakaya 最近の関心ごと:インタラクテションデザイン 資格:ラオスの象使い 自己紹介
abe 君と yashi さんのこのリクエストをそろそろ回収しようと思って... Motivation
• Go ってなんか並行処理が得意らしいじゃん? • C10K 問題を解決できるレベルの性能らしいじゃん? Motivation
→ Go は言語レベルでマルチスレッドをサポートしている • Go ってなんか並行処理が得意らしいじゃん? • C10K 問題を解決できるレベルの性能らしいじゃん? Motivation
• そもそも並行処理とは • Go における並行処理 Ajenda
並行(concurrent): • 複数の仕事を切り替えることで同時にやってい るように見えること • ある一つの時点では一つの仕事しかしてない 並行と並列 並列(parallel): • ある一つの時点で物理的に複数の仕事をして
いること time time
シングルコア: • 一つのコアを持つプロセッサ • 元々は全てシングルコアだった • 同時に一つのタスクしか実行できない • クロック数を上げることで高速化を図っていた 並行と並列
マルチコア: • 複数のコアを持つプロセッサ • デュアルコアとかクワッドコアとか • コアの数だけ同時並列にタスクを実行可能 • 現代の PC は大体マルチコア ※スパコン富岳は52コア https://www.r-ccs.riken.jp/newsletter/202003/interview.html CPU Core CPU Core Core Core Core
• 手元で ps -x コマンドを実行すると大量にプロセスが出てくるはず • コア数分までは同時並列に実行されていて、それ以上は並行的に逐次実行されている • top コマンドの出力を眺めていると、CPU
を消費するプロセス/OSスレッドが逐次切り替わっているのがわかる 並行と並列
• 並行性はコードの性質を指す • 並列性はプログラムのランタイムの性質であり、コードの性質ではない ◦ 並列に動いて欲しいコードを書いても、必ずしも並列に動くわけではない 並行と並列 同時に物事考えたかったら もっと脳みそ連れてこいや
プロセス: • タスクの実行単位 • プロセスごとに独立したメモリ領域 プロセスとスレッド スレッド: • プロセスよりも軽量な実行単位 •
プロセスによって管理される • スレッド間のメモリ共有 • 一口にスレッドと言ってもいくつか種類がある
ネイティブスレッド: • 俗に言う OS スレッド • スレッドの切り替えをカーネルが行うため、複数 のスレッドを並行して実行可能 • マルチコアであれば並列に実行可能
• カーネルスレッドやライトウェイトプロセスなどの 分類がある ネイティブスレッドとユーザースレッド ユーザースレッド: • ユーザー空間で実装されたスレッド • Java の jvm など仮想マシン上で管理されるも のはグリーンスレッドとも呼ばれる • ネイティブスレッドをより抽象化した概念 • あくまでも擬似的なスレッドなので、これ単体で は並列処理は無理
サブルーチン: • 一つのまとまった手続きを指す • いわゆる関数(function) • 処理の start から return
まで一気に行われる サブルーチンとコルーチン コルーチン: • 処理を中断し、続きから再開できる関数 • C# や Lua など一部の言語で提供されている • 原理的にはネイティブスレッドで同じことができ るが、切り替えのオーバーヘッドが少ない
ポイント: • 待ちが少なくなる(= マシンリソースを効率よく使える) • 早く終わるかもしれない(速くはならない) 並行処理のうれしさ
直列の場合 並行処理のうれしさ 待ち時間10分 作業時間5分 切り替え1分 エビフライと野菜炒めができるのに 16分かかる
直列の場合 並行処理のうれしさ 待ち時間10分 作業時間5分 切り替え1分 エビフライと野菜炒めができるのに 16分かかる 無心で油の中のエビを 見守る10分間
並行の場合 並行処理のうれしさ 経過時間7分 作業時間5分 切り替え1分 エビフライと野菜炒めが 10分で完成!6分短縮できた! 切り替え1分 待ち時間3分 待ち時間に別の
タスクを行う
並行の場合 並行処理のうれしさ 経過時間7分 作業時間5分 切り替え1分 エビフライと野菜炒めが 10分で完成!6分短縮できた! 切り替え1分 待ち時間3分 待ち時間に別の
タスクを行う 個々の作業のスピードそのものが速く なったわけではないことに注意
ポイント: • 競合(デッドロック、ライブロック) • 共有メモリアクセス • コンテキストスイッチのオーバーヘッドによる性能劣化 ◦ 収穫逓減の法則 並行処理の難しさ
デッドロック: 並行処理の難しさ TaskA リソースA リソースB TaskB ロック ロック TaskA はリソースBのロック解放を待ち、
TaskB はリソースAのロック解放を待つの で一生処理が進まない
お互いが譲り合って 処理が進まなくなる ライブロック: 並行処理の難しさ TaskA リソースA リソースB TaskB リソースAを 使いたい
リソースAを 使いたい リソースBを 使いたい リソースBを 使いたい TaskB が使うっ ぽいからやめよ TaskB が使うっ ぽいからやめよ リソースAを 使いたい TaskB が使うっ ぽいからやめよ TaskA が使うっ ぽいからやめよ TaskA が使うっ ぽいからやめよ リソースAを 使いたい TaskA が使うっ ぽいからやめよ 以後ループ 以後ループ
共有メモリアクセス: 並行処理の難しさ TaskA リソースA TaskB 参照と更新がアトミックでないため、 1400にならないといけないところが 1200になってしまう 1000 1000
1200 1200 1000+200 1000+200
コンテキストスイッチによる性能劣化: • 実行するタスクを切り替えることをコンテキストスイッチという • スイッチングにはコストがかかる • コストを払っても並行処理した方がいいのは CPU のアイドルタイムを減らせるため •
逆に言うと、CPU の演算を必要とするタスクばかりの場合、スイッチングコストがペイできなくなる 並行処理の難しさ
コンテキストスイッチによる性能劣化: 並行処理の難しさ 作業時間5分 切替時間1分 野菜炒めの調理には CPU の演算のみを必要とするものとする 直列の場合 → 5分+5分+1分(切替1回)=11分
並行した場合 → 5分+5分+5分(切替5回)=15分 スイッチングコストにより逆に遅くなる
Go における並行処理
登場人物: • Goroutine • WaitGroup • Channel • Mutex •
Context • M:Nスケジューラ Go の並行処理
Goroutineとは: • Go が並行処理を扱うための軽量スレッドみたいなもの ◦ グリーンスレッドというより、考え方としてはコルーチン • 一般的なコルーチンとは違い、割り込みや再エントリをプログラマが意識しなくて良い ◦ Go
のランタイムがよしなにやってくれる • ネイティブスレッドと比べてメモリ消費が少なく、コンテキストスイッチも格段に速い ◦ 一つ当たり数キロバイトという軽さ、ミリオン数実行可能 • Go の net/http サーバーはリクエストごとに Goroutine を起動して、マルチプレキシングする作り Go の並行処理
Goroutineの生成: • go キーワードで Goroutine を生成できる ◦ めちゃ簡単!素敵! • Main
関数を実行する Goroutine と sayHello を実行 する Goroutine が生成される Go の並行処理 func main(){ go sayHello() // 他の処理をする... } func sayHello(){ fmt.Println("hello") }
Goroutineの生成: • 無名関数でも可能 Go の並行処理 func main() { go func(){
fmt.Println("hello") }() // 他の処理をする... }
Goroutineの待ち合わせ: • main 関数が終了した時点で Go のプログラムは 終了する • 右のコードではほとんどの場合、sayHello がスケ
ジューリングだけされて実行されない • 他にもバリア同期するために他の Goroutine を待 ちたいとかって時もある • ので、待ち合わせる必要がある Go の並行処理 func main(){ go sayHello() } func sayHello(){ fmt.Println("hello") }
Goroutineの待ち合わせ: • fork-join モデル ◦ 呼び出し元で合流ポイントを作ることで実 行を待つことが可能 • sync package
の WaitGroup を使うことが多い Go の並行処理 func main() { var wg sync.WaitGroup wg.Add(1) // 待つ Goroutine の数を指定 go sayHello(&wg) // Goroutine を生成 wg.Wait() // ここでブロック } func sayHello(wg *sync.WaitGroup) { fmt.Println("hello") wg.Done() // WaitGroup をインクリメント }
Channel: • Goroutine 間でデータをやりとりする機構 • CSP(Communicating Sequential Processes) に由来する並行処理のプリミティブの一つ ◦
(Ruby3 の Ractor の channel は Actor 由来) • FIFO 型のデータ構造のパイプみたいなもの • 共有メモリの参照ではなく所有権の受け渡しを明示的に行うので、スレッドセーフな状態を簡単に作れる Go の並行処理
Channelを使った値の送受信: • make 関数で channel を作成 • <- で値を送受信する •
通常、Channel は値を受信するまでブロックする ので、待つ必要はない • 同じメモリを共有するのではなく、データの所有権 を渡すことで競合を発生しづらくする • 型安全 Go の並行処理 func main() { ch := make(chan string) // channel を作成 go sayHello(ch) v := <-ch // channel から値を受信 fmt.Println(v) } func sayHello(ch chan string) { ch <- "hello" // channel に値を送信 }
Channelを使った値の送受信: • ブロックしたくない場合は select を使う • 右のコードでは select 実行時に channel
に値が 入っていれば “hello” が、なければ “no data” が 出力される Go の並行処理 func main() { ch := make(chan string) // channel を作成 go sayHello(ch) select { case v := <-ch: // 受信できたケース fmt.Println(v) default: // どのケースにも当てはまらない場合 fmt.Println("no data") } } func sayHello(ch chan string) { ch <- "hello" // channel に値を送信 }
Channelを使った値の送受信: • select は複数の channel をまとめる糊のようなも の • こんな感じで複数 Goroutine
作っておいて、先に 返ってきた値を元に処理をしたりといった使い方が できる Go の並行処理 func main() { ch := make(chan string) ch2 := make(chan string) go sayHello(ch) go sayHello(ch2) select { case v := <-ch: fmt.Println(v) case v := <-ch2: fmt.Println(v) } } func sayHello(ch chan string) { ch <- "hello" }
Channelを使った値の送受信: • for-loop で無限ループして、Goroutine の結果を 待ちながら別の処理を行うみたいなこともできる Go の並行処理 func main()
{ ch := make(chan string) go sayHello(ch) for { select { case v := <-ch: fmt.Println(v) default: } // 何か別の処理をする } } func doSomethingHeavy(ch chan string) { // 何か重たい処理をする ch <- "hello" }
Channelを使った値の送受信: • 送信専用、受信専用、バッファ付きなど様々な種類の channel がある • パイプラインやファンイン/ファンアウトみたいな処理も簡単に書ける Go の並行処理
クリティカルセクションの保護: • クリティカルセクションとは、同時にアクセスすることで不具合が生じる危険がある箇所のこと ◦ レースコンディションになるところ全般 • メモリ同期やセマフォなどで明示的に排他制御を行う必要がある Go の並行処理
クリティカルセクションの保護: • channel を使わない場合、クリティカルセクション の排他制御は明示的に行う必要がある • 右のコードでは出力が1000にならずに、実行す るたびに結果が変化する ◦ レースコンディションが発生している
Go の並行処理 func main() { var count int for i := 0; i < 1000; i++ { go func() { count++ }() } time.Sleep(time.Second * 10) fmt.Println(count) }
クリティカルセクションの保護: • Mutex(Mutual Exclusion) を使う • インクリメント処理をロックの中で行う • ログライブラリなどは書き込みで競合しないよう 内部的にロックを取る仕組みになっているので複
数 goroutine で呼び出しても安全 • キャッシュや状態管理に使われる Go の並行処理 func main() { var count int var mu sync.Mutex for i := 0; i < 1000; i++ { go func() { mu.Lock() // 排他ロック count++ mu.Unlock() // ロック解除 }() } time.Sleep(time.Second * 3) fmt.Println(count) }
処理のキャンセル: • 外部 API コールを別 Goroutine で行いたいが、一定時間でタイムアウトさせたいとか • 複数 Goroutine
で並行処理させてるうちのどれか一つでもエラーが発生したら他をキャンセルしたいとか Go の並行処理
channel によるキャンセル: • キャンセル通知用の channel を作り、そこに通知 する方法 Go の並行処理 func
main() { cancel := make(chan struct{}) go infiniteSayHello(cancel) // Goroutine 生成 time.Sleep(3 * time.Second) // 3秒待って close(cancel) // channel にクローズを通知 fmt.Println("finish") } func infiniteSayHello(cancel chan struct{}) { // 無限ループしながら hello を出力する for { select { case <-cancel: // 受信したらリターン return default: fmt.Println("hello") } } }
context によるキャンセル: • context はデッドラインやキャンセル、その他のリ クエストスコープな値を管理するもの • 右のコードでは1秒後に context がタイムアウト
し、ctx.Done() を受信し sayHello が終了するの で、”hello” は出力されない Go の並行処理 func main() { ctx := context.Background() // 1秒でタイムアウトするよう設定 ctx, cancel := context.WithTimeout(ctx, 1*time.Second) defer cancel() go sayHello(ctx) // context を渡す time.Sleep(3 * time.Second) } func sayHello(ctx context.Context) { select { case <-ctx.Done(): // cancel が呼ばれた時の処理 fmt.Println("cancel") return case <-time.After(2 * time.Second): fmt.Println("hello") } }
context によるキャンセル(Case1): • ctx0 から ctx1 を作って Goroutine に渡す •
右の状態で ctx1 をキャンセルする Go の並行処理 Goroutine ctx0 => ctx1 Goroutine Goroutine ctx1 ctx1
context によるキャンセル(Case1): • ctx0 から ctx1 を作って Goroutine に渡す •
右の状態で ctx1 をキャンセルする Go の並行処理 Goroutine ctx0 => ctx1 Goroutine Goroutine ctx1 ctx1 両方キャンセルされる
context によるキャンセル(Case2): • ctx0 から ctx1, ctx2 を作って Goroutine に渡す
• 右の状態で ctx1 をキャンセルする Go の並行処理 Goroutine ctx0 => ctx1, ctx2 Goroutine Goroutine ctx1 ctx2
context によるキャンセル(Case2): • ctx0 から ctx1, ctx2 を作って Goroutine に渡す
• 右の状態で ctx1 をキャンセルする Go の並行処理 Goroutine ctx0 => ctx1, ctx2 Goroutine Goroutine ctx1 ctx2 ctx1 を持つ Goroutine だ けがキャンセルされる
context によるキャンセル(Case3): • ctx0 から ctx1を作って Goroutine に渡す • さらに
ctx1を渡して Goroutine を作る • 右の状態で ctx1 をキャンセルする Go の並行処理 Goroutine ctx0 => ctx1 Goroutine Goroutine ctx1 ctx1
context によるキャンセル(Case3): • ctx0 から ctx1を作って Goroutine に渡す • さらに
ctx1を渡して Goroutine を作る • 右の状態で ctx1 をキャンセルする Go の並行処理 Goroutine ctx0 => ctx1 Goroutine Goroutine ctx1 ctx1 ctx1 を持つ Goroutine が キャンセルされる
context によるキャンセル(Case4): • ctx0 から ctx1を作って Goroutine に渡す • さらに
ctx1 から ctx2を作って Goroutine に渡す • 右の状態で ctx1 をキャンセルする Go の並行処理 Goroutine ctx0 => ctx1 Goroutine ctx1 => ctx2 Goroutine ctx1 ctx2
context によるキャンセル(Case4): • ctx0 から ctx1を作って Goroutine に渡す • さらに
ctx1 から ctx2を作って Goroutine に渡す • 右の状態で ctx1 をキャンセルする Go の並行処理 Goroutine ctx0 => ctx1 Goroutine ctx1 => ctx2 Goroutine ctx1 ctx2 ctx1 とそこから派生した ctx2 を持つ Goroutine がキャンセルされる
M:Nスケジューラ: • N個のOSスレッドに 、M個のユーザースレッド(Goroutine)をマッピングする • マルチコアにスケールし、マシンリソースを効率よく使うことができる Go の並行処理
Go の並行処理 M:Nスケジューラ: • OS にもスケジューリングの機構はあるが... • Go のユーザースレッド側でスケジューリングを行うのは効率がいいから ◦
コンテキストスイッチコストの削減 ▪ OS スレッドの切り替えは比較的オーバーヘッドが大きい ◦ Go ランタイム都合のスケジューリングが可能 ▪ 例えばガベコレの割り込みなどを任意のタイミングですることができる
スケジューリングの登場人物: • M(Machine) ◦ OS スレッド • G(Goroutine) • P(Processor)
◦ GをMに割り当てる ◦ P をいくつ起動できるかは環境変数 `GOMAXPROCS` で定義される ◦ デフォルトはコア数と同一 Go の並行処理
スケジューリング: • イメージはこんな感じ • 通常、PとMが1対1でマッピングされ、それごと に Goroutine を入れるキューがある • 保持する
Goroutine の数が少なくなると他の キューから盗むことで平準化を図る • Goroutine はプリエンプティブ • なので正確にはタスクではなく継続を盗んでい る Go の並行処理 ◦ 出典: 並行処理を支える Goランタイム| Goでの並行処理を徹底解剖! M(Machie) : G(Goroutine) : P(Processor)
• Go は Goroutine (ユーザースレッド)とそのスケジューリングによって効率的な並行処理を実現している • Goroutine 間の値の受け渡しやイベントの伝播など、並行処理に必要なプリミティブを豊富に提供してくれる • 理解は必要だが、意識はしなくて良いので楽に並行プログラムが書ける
◦ 意識するのは Goroutine のみで、OS 側のランタイム事情を考えなくても最適化される まとめ
• 正直、LT 枠に合わせたらめちゃくちゃ薄い内容になりました... • 興味が湧いたらぜひ Go を書こう!! • 参考 終わりに
fin