Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Go と並行処理

nakaryooo
December 03, 2021

Go と並行処理

nakaryooo

December 03, 2021
Tweet

More Decks by nakaryooo

Other Decks in Technology

Transcript

  1. シングルコア: • 一つのコアを持つプロセッサ • 元々は全てシングルコアだった • 同時に一つのタスクしか実行できない • クロック数を上げることで高速化を図っていた 並行と並列

    マルチコア: • 複数のコアを持つプロセッサ • デュアルコアとかクワッドコアとか • コアの数だけ同時並列にタスクを実行可能 • 現代の PC は大体マルチコア ※スパコン富岳は52コア https://www.r-ccs.riken.jp/newsletter/202003/interview.html CPU Core CPU Core Core Core Core
  2. プロセス: • タスクの実行単位 • プロセスごとに独立したメモリ領域 プロセスとスレッド スレッド: • プロセスよりも軽量な実行単位 •

    プロセスによって管理される • スレッド間のメモリ共有 • 一口にスレッドと言ってもいくつか種類がある
  3. ネイティブスレッド: • 俗に言う OS スレッド • スレッドの切り替えをカーネルが行うため、複数 のスレッドを並行して実行可能 • マルチコアであれば並列に実行可能

    • カーネルスレッドやライトウェイトプロセスなどの 分類がある ネイティブスレッドとユーザースレッド ユーザースレッド: • ユーザー空間で実装されたスレッド • Java の jvm など仮想マシン上で管理されるも のはグリーンスレッドとも呼ばれる • ネイティブスレッドをより抽象化した概念 • あくまでも擬似的なスレッドなので、これ単体で は並列処理は無理
  4. サブルーチン: • 一つのまとまった手続きを指す • いわゆる関数(function) • 処理の start から return

    まで一気に行われる サブルーチンとコルーチン コルーチン: • 処理を中断し、続きから再開できる関数 • C# や Lua など一部の言語で提供されている • 原理的にはネイティブスレッドで同じことができ るが、切り替えのオーバーヘッドが少ない
  5. お互いが譲り合って 処理が進まなくなる ライブロック: 並行処理の難しさ TaskA リソースA リソースB TaskB リソースAを 使いたい

    リソースAを 使いたい リソースBを 使いたい リソースBを 使いたい TaskB が使うっ ぽいからやめよ TaskB が使うっ ぽいからやめよ リソースAを 使いたい TaskB が使うっ ぽいからやめよ TaskA が使うっ ぽいからやめよ TaskA が使うっ ぽいからやめよ リソースAを 使いたい TaskA が使うっ ぽいからやめよ 以後ループ 以後ループ
  6. 登場人物: • Goroutine • WaitGroup • Channel • Mutex •

    Context • M:Nスケジューラ Go の並行処理
  7. Goroutineとは: • Go が並行処理を扱うための軽量スレッドみたいなもの ◦ グリーンスレッドというより、考え方としてはコルーチン • 一般的なコルーチンとは違い、割り込みや再エントリをプログラマが意識しなくて良い ◦ Go

    のランタイムがよしなにやってくれる • ネイティブスレッドと比べてメモリ消費が少なく、コンテキストスイッチも格段に速い ◦ 一つ当たり数キロバイトという軽さ、ミリオン数実行可能 • Go の net/http サーバーはリクエストごとに Goroutine を起動して、マルチプレキシングする作り Go の並行処理
  8. Goroutineの生成: • go キーワードで Goroutine を生成できる ◦ めちゃ簡単!素敵! • Main

    関数を実行する Goroutine と sayHello を実行 する Goroutine が生成される Go の並行処理 func main(){ go sayHello() // 他の処理をする... } func sayHello(){ fmt.Println("hello") }
  9. Goroutineの待ち合わせ: • main 関数が終了した時点で Go のプログラムは 終了する • 右のコードではほとんどの場合、sayHello がスケ

    ジューリングだけされて実行されない • 他にもバリア同期するために他の Goroutine を待 ちたいとかって時もある • ので、待ち合わせる必要がある Go の並行処理 func main(){ go sayHello() } func sayHello(){ fmt.Println("hello") }
  10. 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 をインクリメント }
  11. Channel: • Goroutine 間でデータをやりとりする機構 • CSP(Communicating Sequential Processes) に由来する並行処理のプリミティブの一つ ◦

    (Ruby3 の Ractor の channel は Actor 由来) • FIFO 型のデータ構造のパイプみたいなもの • 共有メモリの参照ではなく所有権の受け渡しを明示的に行うので、スレッドセーフな状態を簡単に作れる Go の並行処理
  12. 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 に値を送信 }
  13. 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 に値を送信 }
  14. 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" }
  15. 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" }
  16. クリティカルセクションの保護: • 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) }
  17. 処理のキャンセル: • 外部 API コールを別 Goroutine で行いたいが、一定時間でタイムアウトさせたいとか • 複数 Goroutine

    で並行処理させてるうちのどれか一つでもエラーが発生したら他をキャンセルしたいとか Go の並行処理
  18. 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") } } }
  19. 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") } }
  20. context によるキャンセル(Case1): • ctx0 から ctx1 を作って Goroutine に渡す •

    右の状態で ctx1 をキャンセルする Go の並行処理 Goroutine ctx0 => ctx1 Goroutine Goroutine ctx1 ctx1
  21. context によるキャンセル(Case1): • ctx0 から ctx1 を作って Goroutine に渡す •

    右の状態で ctx1 をキャンセルする Go の並行処理 Goroutine ctx0 => ctx1 Goroutine Goroutine ctx1 ctx1 両方キャンセルされる
  22. context によるキャンセル(Case2): • ctx0 から ctx1, ctx2 を作って Goroutine に渡す

    • 右の状態で ctx1 をキャンセルする Go の並行処理 Goroutine ctx0 => ctx1, ctx2 Goroutine Goroutine ctx1 ctx2
  23. context によるキャンセル(Case2): • ctx0 から ctx1, ctx2 を作って Goroutine に渡す

    • 右の状態で ctx1 をキャンセルする Go の並行処理 Goroutine ctx0 => ctx1, ctx2 Goroutine Goroutine ctx1 ctx2 ctx1 を持つ Goroutine だ けがキャンセルされる
  24. context によるキャンセル(Case3): • ctx0 から ctx1を作って Goroutine に渡す • さらに

    ctx1を渡して Goroutine を作る • 右の状態で ctx1 をキャンセルする Go の並行処理 Goroutine ctx0 => ctx1 Goroutine Goroutine ctx1 ctx1
  25. context によるキャンセル(Case3): • ctx0 から ctx1を作って Goroutine に渡す • さらに

    ctx1を渡して Goroutine を作る • 右の状態で ctx1 をキャンセルする Go の並行処理 Goroutine ctx0 => ctx1 Goroutine Goroutine ctx1 ctx1 ctx1 を持つ Goroutine が キャンセルされる
  26. context によるキャンセル(Case4): • ctx0 から ctx1を作って Goroutine に渡す • さらに

    ctx1 から ctx2を作って Goroutine に渡す • 右の状態で ctx1 をキャンセルする Go の並行処理 Goroutine ctx0 => ctx1 Goroutine ctx1 => ctx2 Goroutine ctx1 ctx2
  27. context によるキャンセル(Case4): • ctx0 から ctx1を作って Goroutine に渡す • さらに

    ctx1 から ctx2を作って Goroutine に渡す • 右の状態で ctx1 をキャンセルする Go の並行処理 Goroutine ctx0 => ctx1 Goroutine ctx1 => ctx2 Goroutine ctx1 ctx2 ctx1 とそこから派生した ctx2 を持つ Goroutine がキャンセルされる
  28. Go の並行処理 M:Nスケジューラ: • OS にもスケジューリングの機構はあるが... • Go のユーザースレッド側でスケジューリングを行うのは効率がいいから ◦

    コンテキストスイッチコストの削減 ▪ OS スレッドの切り替えは比較的オーバーヘッドが大きい ◦ Go ランタイム都合のスケジューリングが可能 ▪ 例えばガベコレの割り込みなどを任意のタイミングですることができる
  29. スケジューリングの登場人物: • M(Machine) ◦ OS スレッド • G(Goroutine) • P(Processor)

    ◦ GをMに割り当てる ◦ P をいくつ起動できるかは環境変数 `GOMAXPROCS` で定義される ◦ デフォルトはコア数と同一 Go の並行処理
  30. スケジューリング: • イメージはこんな感じ • 通常、PとMが1対1でマッピングされ、それごと に Goroutine を入れるキューがある • 保持する

    Goroutine の数が少なくなると他の キューから盗むことで平準化を図る • Goroutine はプリエンプティブ • なので正確にはタスクではなく継続を盗んでい る Go の並行処理 ◦ 出典: 並行処理を支える Goランタイム| Goでの並行処理を徹底解剖! M(Machie) : G(Goroutine) : P(Processor)
  31. fin