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

Go と並行処理

RyotaNakaya
December 03, 2021

Go と並行処理

RyotaNakaya

December 03, 2021
Tweet

More Decks by RyotaNakaya

Other Decks in Technology

Transcript

  1. 2021/12/03 Nakaya Ryota
    Goと並行処理

    View full-size slide

  2. ギフティ入社:2019年1月
    所属:ContentsCreation Div. ProductUnit2(コーヒー屋さんチーム)
    前職:バックオフィス系システムのパッケージベンダー(上流メイン)
    社内部活:#coffee、#darts、#poker、#among_us…
    分報:#times_nakaya
    最近の関心ごと:インタラクテションデザイン
    資格:ラオスの象使い
    自己紹介

    View full-size slide

  3. abe 君と yashi さんのこのリクエストをそろそろ回収しようと思って...
    Motivation

    View full-size slide

  4. ● Go ってなんか並行処理が得意らしいじゃん?
    ● C10K 問題を解決できるレベルの性能らしいじゃん?
    Motivation

    View full-size slide

  5. → Go は言語レベルでマルチスレッドをサポートしている
    ● Go ってなんか並行処理が得意らしいじゃん?
    ● C10K 問題を解決できるレベルの性能らしいじゃん?
    Motivation

    View full-size slide

  6. ● そもそも並行処理とは
    ● Go における並行処理
    Ajenda

    View full-size slide

  7. 並行(concurrent):
    ● 複数の仕事を切り替えることで同時にやってい
    るように見えること
    ● ある一つの時点では一つの仕事しかしてない
    並行と並列
    並列(parallel):
    ● ある一つの時点で物理的に複数の仕事をして
    いること
    time time

    View full-size slide

  8. シングルコア:
    ● 一つのコアを持つプロセッサ
    ● 元々は全てシングルコアだった
    ● 同時に一つのタスクしか実行できない
    ● クロック数を上げることで高速化を図っていた
    並行と並列
    マルチコア:
    ● 複数のコアを持つプロセッサ
    ● デュアルコアとかクワッドコアとか
    ● コアの数だけ同時並列にタスクを実行可能
    ● 現代の PC は大体マルチコア
    ※スパコン富岳は52コア
    https://www.r-ccs.riken.jp/newsletter/202003/interview.html
    CPU
    Core
    CPU
    Core Core
    Core Core

    View full-size slide

  9. ● 手元で ps -x コマンドを実行すると大量にプロセスが出てくるはず
    ● コア数分までは同時並列に実行されていて、それ以上は並行的に逐次実行されている
    ● top コマンドの出力を眺めていると、CPU を消費するプロセス/OSスレッドが逐次切り替わっているのがわかる
    並行と並列

    View full-size slide

  10. ● 並行性はコードの性質を指す
    ● 並列性はプログラムのランタイムの性質であり、コードの性質ではない
    ○ 並列に動いて欲しいコードを書いても、必ずしも並列に動くわけではない
    並行と並列
    同時に物事考えたかったら
    もっと脳みそ連れてこいや

    View full-size slide

  11. プロセス:
    ● タスクの実行単位
    ● プロセスごとに独立したメモリ領域
    プロセスとスレッド
    スレッド:
    ● プロセスよりも軽量な実行単位
    ● プロセスによって管理される
    ● スレッド間のメモリ共有
    ● 一口にスレッドと言ってもいくつか種類がある

    View full-size slide

  12. ネイティブスレッド:
    ● 俗に言う OS スレッド
    ● スレッドの切り替えをカーネルが行うため、複数
    のスレッドを並行して実行可能
    ● マルチコアであれば並列に実行可能
    ● カーネルスレッドやライトウェイトプロセスなどの
    分類がある
    ネイティブスレッドとユーザースレッド
    ユーザースレッド:
    ● ユーザー空間で実装されたスレッド
    ● Java の jvm など仮想マシン上で管理されるも
    のはグリーンスレッドとも呼ばれる
    ● ネイティブスレッドをより抽象化した概念
    ● あくまでも擬似的なスレッドなので、これ単体で
    は並列処理は無理

    View full-size slide

  13. サブルーチン:
    ● 一つのまとまった手続きを指す
    ● いわゆる関数(function)
    ● 処理の start から return まで一気に行われる
    サブルーチンとコルーチン
    コルーチン:
    ● 処理を中断し、続きから再開できる関数
    ● C# や Lua など一部の言語で提供されている
    ● 原理的にはネイティブスレッドで同じことができ
    るが、切り替えのオーバーヘッドが少ない

    View full-size slide

  14. ポイント:
    ● 待ちが少なくなる(= マシンリソースを効率よく使える)
    ● 早く終わるかもしれない(速くはならない)
    並行処理のうれしさ

    View full-size slide

  15. 直列の場合
    並行処理のうれしさ
    待ち時間10分 作業時間5分
    切り替え1分
    エビフライと野菜炒めができるのに 16分かかる

    View full-size slide

  16. 直列の場合
    並行処理のうれしさ
    待ち時間10分 作業時間5分
    切り替え1分
    エビフライと野菜炒めができるのに 16分かかる
    無心で油の中のエビを
    見守る10分間

    View full-size slide

  17. 並行の場合
    並行処理のうれしさ
    経過時間7分
    作業時間5分
    切り替え1分
    エビフライと野菜炒めが 10分で完成!6分短縮できた!
    切り替え1分
    待ち時間3分
    待ち時間に別の
    タスクを行う

    View full-size slide

  18. 並行の場合
    並行処理のうれしさ
    経過時間7分
    作業時間5分
    切り替え1分
    エビフライと野菜炒めが 10分で完成!6分短縮できた!
    切り替え1分
    待ち時間3分
    待ち時間に別の
    タスクを行う
    個々の作業のスピードそのものが速く
    なったわけではないことに注意

    View full-size slide

  19. ポイント:
    ● 競合(デッドロック、ライブロック)
    ● 共有メモリアクセス
    ● コンテキストスイッチのオーバーヘッドによる性能劣化
    ○ 収穫逓減の法則
    並行処理の難しさ

    View full-size slide

  20. デッドロック:
    並行処理の難しさ
    TaskA
    リソースA リソースB
    TaskB
    ロック
    ロック
    TaskA はリソースBのロック解放を待ち、
    TaskB はリソースAのロック解放を待つの
    で一生処理が進まない

    View full-size slide

  21. お互いが譲り合って
    処理が進まなくなる
    ライブロック:
    並行処理の難しさ
    TaskA
    リソースA リソースB
    TaskB
    リソースAを
    使いたい
    リソースAを
    使いたい
    リソースBを
    使いたい
    リソースBを
    使いたい
    TaskB が使うっ
    ぽいからやめよ
    TaskB が使うっ
    ぽいからやめよ
    リソースAを
    使いたい
    TaskB が使うっ
    ぽいからやめよ
    TaskA が使うっ
    ぽいからやめよ
    TaskA が使うっ
    ぽいからやめよ
    リソースAを
    使いたい
    TaskA が使うっ
    ぽいからやめよ
    以後ループ
    以後ループ

    View full-size slide

  22. 共有メモリアクセス:
    並行処理の難しさ
    TaskA
    リソースA
    TaskB
    参照と更新がアトミックでないため、
    1400にならないといけないところが
    1200になってしまう
    1000
    1000
    1200
    1200
    1000+200
    1000+200

    View full-size slide

  23. コンテキストスイッチによる性能劣化:
    ● 実行するタスクを切り替えることをコンテキストスイッチという
    ● スイッチングにはコストがかかる
    ● コストを払っても並行処理した方がいいのは CPU のアイドルタイムを減らせるため
    ● 逆に言うと、CPU の演算を必要とするタスクばかりの場合、スイッチングコストがペイできなくなる
    並行処理の難しさ

    View full-size slide

  24. コンテキストスイッチによる性能劣化:
    並行処理の難しさ
    作業時間5分
    切替時間1分
    野菜炒めの調理には CPU の演算のみを必要とするものとする
    直列の場合
    → 5分+5分+1分(切替1回)=11分
    並行した場合
    → 5分+5分+5分(切替5回)=15分
    スイッチングコストにより逆に遅くなる

    View full-size slide

  25. Go における並行処理

    View full-size slide

  26. 登場人物:
    ● Goroutine
    ● WaitGroup
    ● Channel
    ● Mutex
    ● Context
    ● M:Nスケジューラ
    Go の並行処理

    View full-size slide

  27. Goroutineとは:
    ● Go が並行処理を扱うための軽量スレッドみたいなもの
    ○ グリーンスレッドというより、考え方としてはコルーチン
    ● 一般的なコルーチンとは違い、割り込みや再エントリをプログラマが意識しなくて良い
    ○ Go のランタイムがよしなにやってくれる
    ● ネイティブスレッドと比べてメモリ消費が少なく、コンテキストスイッチも格段に速い
    ○ 一つ当たり数キロバイトという軽さ、ミリオン数実行可能
    ● Go の net/http サーバーはリクエストごとに Goroutine を起動して、マルチプレキシングする作り
    Go の並行処理

    View full-size slide

  28. Goroutineの生成:
    ● go キーワードで Goroutine を生成できる
    ○ めちゃ簡単!素敵!
    ● Main 関数を実行する Goroutine と sayHello を実行
    する Goroutine が生成される
    Go の並行処理
    func main(){
    go sayHello()
    // 他の処理をする...
    }
    func sayHello(){
    fmt.Println("hello")
    }

    View full-size slide

  29. Goroutineの生成:
    ● 無名関数でも可能
    Go の並行処理
    func main() {
    go func(){
    fmt.Println("hello")
    }()
    // 他の処理をする...
    }

    View full-size slide

  30. Goroutineの待ち合わせ:
    ● main 関数が終了した時点で Go のプログラムは
    終了する
    ● 右のコードではほとんどの場合、sayHello がスケ
    ジューリングだけされて実行されない
    ● 他にもバリア同期するために他の Goroutine を待
    ちたいとかって時もある
    ● ので、待ち合わせる必要がある
    Go の並行処理
    func main(){
    go sayHello()
    }
    func sayHello(){
    fmt.Println("hello")
    }

    View full-size slide

  31. 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 をインクリメント
    }

    View full-size slide

  32. Channel:
    ● Goroutine 間でデータをやりとりする機構
    ● CSP(Communicating Sequential Processes) に由来する並行処理のプリミティブの一つ
    ○ (Ruby3 の Ractor の channel は Actor 由来)
    ● FIFO 型のデータ構造のパイプみたいなもの
    ● 共有メモリの参照ではなく所有権の受け渡しを明示的に行うので、スレッドセーフな状態を簡単に作れる
    Go の並行処理

    View full-size slide

  33. 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 に値を送信
    }

    View full-size slide

  34. 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 に値を送信
    }

    View full-size slide

  35. 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"
    }

    View full-size slide

  36. 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"
    }

    View full-size slide

  37. Channelを使った値の送受信:
    ● 送信専用、受信専用、バッファ付きなど様々な種類の channel がある
    ● パイプラインやファンイン/ファンアウトみたいな処理も簡単に書ける
    Go の並行処理

    View full-size slide

  38. クリティカルセクションの保護:
    ● クリティカルセクションとは、同時にアクセスすることで不具合が生じる危険がある箇所のこと
    ○ レースコンディションになるところ全般
    ● メモリ同期やセマフォなどで明示的に排他制御を行う必要がある
    Go の並行処理

    View full-size slide

  39. クリティカルセクションの保護:
    ● 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)
    }

    View full-size slide

  40. クリティカルセクションの保護:
    ● 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)
    }

    View full-size slide

  41. 処理のキャンセル:
    ● 外部 API コールを別 Goroutine で行いたいが、一定時間でタイムアウトさせたいとか
    ● 複数 Goroutine で並行処理させてるうちのどれか一つでもエラーが発生したら他をキャンセルしたいとか
    Go の並行処理

    View full-size slide

  42. 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")
    }
    }
    }

    View full-size slide

  43. 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")
    }
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  51. context によるキャンセル(Case4):
    ● ctx0 から ctx1を作って Goroutine に渡す
    ● さらに ctx1 から ctx2を作って Goroutine に渡す
    ● 右の状態で ctx1 をキャンセルする
    Go の並行処理
    Goroutine
    ctx0 => ctx1
    Goroutine
    ctx1 => ctx2
    Goroutine
    ctx1
    ctx2
    ctx1 とそこから派生した ctx2 を持つ
    Goroutine がキャンセルされる

    View full-size slide

  52. M:Nスケジューラ:
    ● N個のOSスレッドに 、M個のユーザースレッド(Goroutine)をマッピングする
    ● マルチコアにスケールし、マシンリソースを効率よく使うことができる
    Go の並行処理

    View full-size slide

  53. Go の並行処理
    M:Nスケジューラ:
    ● OS にもスケジューリングの機構はあるが...
    ● Go のユーザースレッド側でスケジューリングを行うのは効率がいいから
    ○ コンテキストスイッチコストの削減
    ■ OS スレッドの切り替えは比較的オーバーヘッドが大きい
    ○ Go ランタイム都合のスケジューリングが可能
    ■ 例えばガベコレの割り込みなどを任意のタイミングですることができる

    View full-size slide

  54. スケジューリングの登場人物:
    ● M(Machine)
    ○ OS スレッド
    ● G(Goroutine)
    ● P(Processor)
    ○ GをMに割り当てる
    ○ P をいくつ起動できるかは環境変数 `GOMAXPROCS` で定義される
    ○ デフォルトはコア数と同一
    Go の並行処理

    View full-size slide

  55. スケジューリング:
    ● イメージはこんな感じ
    ● 通常、PとMが1対1でマッピングされ、それごと
    に Goroutine を入れるキューがある
    ● 保持する Goroutine の数が少なくなると他の
    キューから盗むことで平準化を図る
    ● Goroutine はプリエンプティブ
    ● なので正確にはタスクではなく継続を盗んでい

    Go の並行処理
    ○ 出典: 並行処理を支える Goランタイム| Goでの並行処理を徹底解剖!
    M(Machie) : G(Goroutine) : P(Processor)

    View full-size slide

  56. ● Go は Goroutine (ユーザースレッド)とそのスケジューリングによって効率的な並行処理を実現している
    ● Goroutine 間の値の受け渡しやイベントの伝播など、並行処理に必要なプリミティブを豊富に提供してくれる
    ● 理解は必要だが、意識はしなくて良いので楽に並行プログラムが書ける
    ○ 意識するのは Goroutine のみで、OS 側のランタイム事情を考えなくても最適化される
    まとめ

    View full-size slide

  57. ● 正直、LT 枠に合わせたらめちゃくちゃ薄い内容になりました...
    ● 興味が湧いたらぜひ Go を書こう!!
    ● 参考
    終わりに

    View full-size slide