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