Goによる並行処理

概要

かかった時間

  • 3.5 時間

読む前の状態

  • 前に一度通読している

  • 基本的なGoの構文(go, chan, select)などはわかる

この本を読んで達成したいことはなにか

  • Goの並行処理を正しく使うための知識(基本的なパターン、例外処理など)を再整理する

  • 並行処理の本質的な理解をする。具体的には以下の質問に答えられるようにする

    • メモリバリア命令の存在理由を説明できる

    • コンパイラのリオーダーを制御する必要性が説明できる

読む前後の変化

読書メモ

第 3 章:Goにおける並行処理の構成要素

3.1 ゴルーチン(Goroutine)

  • ゴルーチンはGoのプログラムで最も基本的な構成要素

  • ゴルーチンはコルーチンとして知られる高度な抽象化

    • コルーチンはプリエンプティブでない並行処理のサブルーチン。割り込みされることがない。

  • GoはM:Nスケジューラでゴルーチンを管理している

    • Mつのグリーンスレッド(ゴルーチンがスケジュールされる)をN個のOSのスレッドに対応させる

  • fork-joinモデル

  • ゴルチーンの中でクロージャーを実行するとき、同じアドレス空間上で実行される

    • 変数の参照など

  • ゴルーチンは生成のコストが小さい(数キロバイト程度)

    • 筆者のマシン(Go 1.13)で計測したときは8kb程度であった

ちなみにサンプルプログラムでは、チャネルをcloseすることで、チャネルから値が送信されるのを待っているゴルーチンにゼロ値を送信するテクニックを使っている。closeされたチャネルからはゼロ値が取得できるため。

3.3 チャネル(channel)

3.4 select文

  • プログラムの処理とチャネルを安全に組み合わせるもの

  • 書き込みや読み込みのチャネルはすべて同時に扱われ、どれが準備できたかを確認する

  • どれも実行できない場合は、select文でブロックされる

  • 選択しているすべてのチャネルがブロックしているときになにかしたい場合のためにdefault statementを用いることができる

  • 複数のチャネルから同時に読み込むことができる場合は一様な選択がされ、ランダムである。

例:selectの動作例

例2:一様なランダムにselectされる例

第 4 章:Goでの並行処理パターン

4.1 拘束

  • 並行処理で安全な操作をする方法

    • メモリを共有するための同期プリミティブ(sync.Mutexなど)

    • 通信による同期(チャネル)

    • イミュータブルなデータ

      • 新しいデータを作りたい場合はデータをコピーしてから操作する

    • 拘束によって保護されたデータ

      • データを唯一の並行プロセスからのみ得られることを確実にする考え方

      • アドホックとレキシカル

        • アドホック拘束とは、規約によって達成すること。静的解析などを利用しなければ規約を守り続けるのは難しい

        • レキシカル拘束とは、レキシカルスコープを使って適切なデータと並行処理のプリミティブだけを複数の並行プロセスが使えるように公開すること

          • チャネルは並行処理で安全に値を渡すことができるが、並行安全でないデータ構造の場合でもレキシカル拘束を用いることができる

      • 同期を使用せずに、拘束を使用する目的は、パフォーマンスの向上と開発者に対する可読性の向上。同期で発生する問題を避けること。

例1:レキシカル拘束の例

例2:レキシカル拘束の例(並行安全でない場合)

4.2 for-selectループ

  • for-selectパターンは以下のパターンを指す

  • パターンを使うときのシナリオ

    • チャネルから繰り返しの変数を送出する

    • 停止シグナルを待つ無限ループ

defautl句を使うときの注意点として、他のケースが成立しないとき間、ビジーループになってしまう。なのでdefault句は書かずにselectでブロックしておくのが良いと思っている。

4.3 ゴルーチンリークを避ける

  • ゴルーチンの生成コストは小さいとはいえ、コストはかかる。ガベージコレクションもされない。

  • ゴルーチンが終了する場合

    • ゴルーチンが処理を完了する場合

    • 回復できないエラーによって処理を続けられない場合

    • 停止するように命令された場合

  • ゴルーチンが終了せずにメモリ上に生き残る場合はゴルーチンリークにつながり、メモリの使用に影響がある。

  • ゴルーチンの親から子へシグナルを送信してキャンセルできるようにすることでゴルーチンリークを避けることができる

例:処理を待っているGoroutineを親のGoroutineがキャンセルする例

チャネルをcloseすることでそのチャネルがアンブロッキングになり、ゼロ値を受信できるようにできるテクニックをここでも使っている。doWork関数内の無名関数はGoroutineで動作する。

  • ゴルーチンがゴルーチンの生成の責任を持っているのであれば、そのゴルーチンを停止できるようにする責任もある

  • doneChを渡す方法が基本的

4.4 orチャネル

  • 1つ以上のdoneチャネルを1つのdoneチャネルにまとめて、まとめているチャネルのうちどれか1つのチャネルが閉じられたら、まとめたチャネルも閉じられるようにしたい

    • selectを使うこともOKだが、いくつのチャネルがあるかわからない時がある

    • or チャネルパターンを用いることができる

  • システム内で複数のモジュールを組み合わせる際の継ぎ目として利用すると便利

    • コールスタック内でゴルーチンの木構造をキャンセルする条件が複数になる傾向があるためorチャネルパターンが便利

例:orチャネルパターンの実装例

4.5 エラーハンドリング

  • 誰がそのエラーを処理する責任を持つべきか

  • 一般的に並行プロセスはエラーを、プログラムの状態を完全に把握していて何をすべきかをより多くの情報に基づいて決定できる別の箇所へと送るべき

  • 取得されるであろう結果とエラーを対にする

  • エラーはゴルーチンからreturnされる値を構築する際の第一級市民として捉えられるべき

例:結果とエラーをまとめる例

関数の返り値を多値で (http.Response, error) などとしたいが、チャネルを用いる場合はこれができないので、上記の実装例のように構造体でまとめる必要があるのだろう。先にチャネルをreturnするが、非同期でGoroutineがHTTPリクエストを発行している。

4.6 パイプライン

  • パイプラインはシステムの抽象化に使える

    • データを受け取って、何からの処理を行って、どこかに渡すという一連の処理

    • 各ステージでの懸念事項を独立させることができる

    • パイプラインの性質

      • ステージは受け取るものと返すものが同じ型である

      • ステージは引き回せるように具体化されていなければならない

例:チャネルを使わないパイプラインの実装例

例:チャネルを使ったパイプラインの実装例

4.7 ファンアウト、ファンイン

  • ファンアウト

    • パイプラインからの入力を扱うために複数のゴルーチンを起動するプロセスを説明する用語

  • ファンイン

    • 複数の結果を1つのチャネルに結合するプロセスを説明する用語

  • ファンアウトを利用するシーン

    • そのステージがより前の計算結果に依存していない

    • 実行が長時間に及ぶ

4.8 or-doneチャネル

  • システムの完全に異なる部分から受け取ったチャネルを扱う場合

    • 例えばdoneChによるゴルーチンのキャンセル

  • コードの可読性が落ちる

  • 以下のようにorDoneの処理を隠蔽化することができる

4.9 teeチャネル

  • チャネルからのストリームを2つに分けて、同じ値を2つの異なる場所で使いたい場合。Unixコマンドのteeのイメージ

4.10 bridgeチャネル

  • チャネルのチャネルを崩して、単一のチャネルにする技

4.11 キュー

  • バッファ付きチャネル

    • 一種のキューだが、導入するのはプログラムを最適化する一番最後

    • キューの導入が早すぎると、デッドロックやライブロックなどの動機に関する問題を隠してしまう

    • キューの実用性は、あるステージの実行時間が他のステージの実行時間に影響を与えないようにステージを分離すること

      • ステージ内でのバッチによるリクエストが時間を節約する場合

      • ステージにおける遅延がシステムにフィードバックループを発生させる場合

    • キューは以下のどちらかのステージで実装されるべき

      • パイプラインの入り口

      • バッチ処理によって効率的になるステージの中

    • リトルの法則(L=λW)

      • L:システムの平均ユニット数

      • λ:ユニットの平均到達率

      • W:ユニットのシステム内での平均滞在時間

4.12 contextパッケージ

  • contextパッケージの2つの目的

    • コールグラフの各枝をキャンセルするAPIを提供

    • コールグラフを通じてリクエストに関するデータを渡すデータの置き場所を提供

  • 関数内でキャンセルするときのシーン

    • ゴルーチンの親がキャンセルをしたい場合

    • ゴルーチンが子がキャンセルをしたい場合

    • ゴルーチン内のブロックしている処理がキャンセルされるように中断できる必要があるとき

  • Contextインターフェースは内部状態を変更できるメソッドを持っていない

    • コールスタックの上記の関数が下位の関数によってコンテキストをキャンセルされてしまうことを防ぐ

例:contextを使ってゴルーチンをキャンセルする実装例

  • データバッグとしてのcontext(TODO)

Last updated