2.17

View in English

2.17 並行性と並列性

概要と動機

並行性とは、互いを待たずに進められる独立したタスクとしてプログラムを構造化する技芸です。並列性とは、それらのタスクを複数のプロセッサで実際に同じ瞬間に実行することです。この区別は衒学的なものではありません。並行性は、遅いネットワーク呼び出しがプログラム全体を固まらせないようにコードを整理する方法であり、並列性は、大きな計算をコアに分散させてより速く終わらせる方法です。両者を混同すると、チームは速度を期待してスレッドを加え、バグだけを受け取ります。

大きなチームにとって、このトピックが重要なのは、並行性が正しさが静かに死ぬ場所だからです。シングルスレッドのコードを書く一人の作者は、行ごとにそれを推論できますが、多くの作者がスレッド間でメモリを共有した瞬間、取りうるインターリーブの数が爆発し、すべてのテストに合格するプログラムが、本番の負荷のもとで百万回に一度、大きなクラッシュではなく、壊れたデータ、固まったリクエスト、誰にも再現できないインシデントとして失敗しえます。本章は、2.16章(パフォーマンスエンジニアリング)のコードレベルの焦点と2.13章の計算の基礎の上に築かれ、マシンをまたぐ並行性に信頼できないネットワークという残酷さが加わる、3.3章(分散システム)の調整の問題に流れ込みます。

企業にとって、並行性のバグはスループットのバグです。高トラフィックのサービスは、共有状態で競合せずに何千もの同時リクエストを扱う能力で浮き沈みし、同期されていないカウンター一つが、負荷のもとで元帳を壊しえます。政府にとって、賭け金は、何十年も動き、安全、給付、公的記録に触れるシステムでの、正しさと監査可能性です。税や医療のシステムでの競合は、不便ではありません。後に誰かが監督機関に説明しなければならない、間違った答えです。どちらの設定でも目標は同じです。安全な経路を既定にして、コードに触れる多くの人々が、それぞれ並行性の専門家である必要がないようにすることです。

主要原則

  • 並行性は構造であり、並列性は実行である。 スレッドに手を伸ばす前に、実際にどちらが必要かを決めます。
  • 共有された可変状態は敵である。 ほぼすべての並行性のバグは、二つのタスクが同じ変更可能なデータに触れることにたどれます。
  • 不変性とメッセージパッシングを好む。 変更できないデータは競合しえず、メッセージは安全性で共有メモリに勝ります。
  • 非決定性が中核の困難である。 千回に一回現れるバグは、エッジケースではなく問題そのものです。
  • すべてに上限を設ける。 制限のないキュー、スレッド数、処理中の仕事は、急増を障害に変えます。
  • 高水準のモデルは、生のロックに勝る。 アクター、チャネル、構造化された並行性は、多くの作者に安全な既定を与え、すべてのロックにはコストがあります。
  • ハッピーパスだけでなく、インターリーブをテストする。 決定的なテストは、まれな順序だけが明らかにするバグを捉えられません。

推奨事項

並行性が必要か並列性が必要かを決める

問題に名前を付けることから始めます。サービスが時間の大半を待つこと(データベース、ネットワーク呼び出し、ディスク)に費やしているなら、I/Oバウンドのワークロードであり、答えは並行性です。一つのリクエストが待つ間に他が進むようにコードを構造化します。async/awaitを使う単一スレッドや小さなプールは、何千もの待機中のリクエストを捌けます。代わりにプログラムがCPUバウンドで、ほとんど待たずに計算を続けているなら、速度を買うのはコアにわたる並列性で、ここでの上限はアムダールの法則(2.16章を参照)で決まります。コアをいくら増やしても、直列の割合が高速化に上限を課します。設計する前に、どちらの領域にいるかを測定してください。

共有された可変状態を敵として扱う

ほぼすべての並行性の欠陥は同じ形に帰着します。二つのタスクが、順序に合意せずに同じ変更可能なデータを読み書きすること。これが競合状態で、失われた更新、書きかけのオブジェクト、コードが安全だと想定した不変条件に違反する値を生みます。最も信頼できる防御は、共有された可変状態を減らすことです。各タスクに自分のデータを与え、参照ではなくコピーを渡し、可変状態を、他がメッセージを通じて届く単一の所有者に閉じ込めます。どうしても共有しなければならないときは、共有を明示的で小さくし、レビュアーが状態に触れるすべての場所を見られるようにします。

不変性とメッセージパッシングを既定として好む

最も安全な共有データは、変更できないデータです。不変オブジェクトは、いったん構築されれば、競合する対象が何もないので、同期なしでいくつのスレッドからでも読めます。不変性を既定とし、可変性を意図した例外にします。タスクが調整しなければならないときは、共有メモリよりメッセージパッシングを好みます。共通の変数を共有する代わりに、一つのタスクがもう一つに値を送ります。それが、Goの格言「メモリを共有して通信するな。通信してメモリを共有せよ」の背後にある哲学です。メッセージパッシングは、見えない順序依存のバグを、明示的で検査できるデータフローに変え、その明晰さは、多くの人が保守するコードでは、メッセージごとのコストにほぼ常に見合います。

生のロックの前に、高水準のモデルに手を伸ばす

手書きのロックは、原則としては正しく、実践では悲惨です。人間は、すべてのインターリーブを推論するのが下手だからです。安全な並行性を既定にするモデルを好みます。アクターモデルは、各アクターに私的な状態とメールボックスを与えます。アクターはメモリを共有せずメッセージを送るだけなので、競合の種類全体が消えます。通信順次プロセス(CSP)は、Goのような言語のチャネルの背後にあるモデルで、独立したプロセスが型付きのチャネルで値を渡します。構造化された並行性は、並行タスクの寿命を字句的なスコープに結びつけ、タスクがそれを生み出したブロックより長生きできず、エラーが消えずに伝播するようにします。async/awaitは、並行でI/Oバウンドなコードを逐次的なスタイルで書けるようにします。これらのどれもが、平均的な作者の床を引き上げ、それこそが大きなチームが必要とするものです。

メモリモデル、原子性、可視性を理解する

メモリを共有するとき、二つの性質が牙をむきます。原子性とは、操作が一度に起こるか、まったく起こらないかです。単純なインクリメント(x = x + 1)は、読み、足し、書く三つのステップが別のスレッドに割り込まれうるので原子的ではなく、それがカウンターが更新を失う理由です。可視性とは、あるスレッドの書き込みが別のスレッドに観測できるようになることです。適切な同期がなければ、あるコアで書かれた値が、別のコアに見えないキャッシュに留まりえるので、すでに設定されたフラグ上でスレッドが永遠にループすることがあります。言語のメモリモデルは、書き込みがいつ見えるようになり、コンパイラとCPUがどの順序を並べ替えてよいかを定義するので、コードが書いた順序で動くと想定できません。独自のロックフリーの方式を発明するのではなく、言語のアトミック型と同期のプリミティブを使います。

同期のプリミティブを意図して使い、デッドロックを避けるよう設計する

共有が避けられないとき、適切なプリミティブに手を伸ばし、その代償を尊重します。ロックやミューテックス(相互排他)は、一度に一つのスレッドだけがクリティカルセクションに入れるようにしますが、アクセスを直列化するので、ホットなロックは多くのコアの利益を消すボトルネックになります。セマフォは、一度に進めるタスクの数を制限し、それがプールに上限を設ける方法です。アトミック操作は、カウンターのような単純な値のロックフリーの更新を提供し、ロックより安いですが、複合的なものには誤用しやすいものです。ロックは三つの古典的な失敗モードをもたらします。デッドロックは、タスクが循環して互いを待ち、どれも進めないことで、教科書的な例は、二つのスレッドがそれぞれ一つのロックを持ち、もう一方を欲しがる場合です。ライブロックは、タスクが互いに反応し続けながら進捗がないことです。飢餓は、他が割り込み続けるために、あるタスクが資源を得られないことです。これらを防ぐ規律は具体的です。グローバルなロックの順序を課す、ロックを短く保つ、止まったタスクが大きな音を立てて失敗するようタイムアウトを加える、ロックを持っている間に未知のコードを呼ばない、飢餓がリスクの所では公平なスケジューリングを使う。新しい作者はコードだけからそれを再発見できないので、これらのルールを書き留めてください。

キュー、プール、処理中の仕事に、バックプレッシャーで上限を設ける

制限のないキューは時限爆弾です。トラフィックの急増のもとで、仕事が排出されるより速く到着し、キューは際限なく伸び、メモリが埋まり、サービスは、それが実際には過負荷であるのに、謎のメモリ不足のクラッシュに見える形で死にます。すべてのキューに上限を設け、すべてのスレッドプールに上限を付け、バックプレッシャーを適用します。システムが満杯のとき、終わらせられない無限の仕事を受け入れるのではなく、上流に遅くするよう合図するか、仕事を素早く拒否します。プールをワークロードに合わせてサイズ決定し(CPUバウンドの仕事ではおおよそコア数、スレッドが大半待つI/Oバウンドの仕事ではそれより高く)、上限を意図したキャパシティの決定として扱います。これは3.3章のレジリエンスのパターンにつながります。

恥ずかしいほど並列な仕事には、データ並列性を使う

一部の問題はきれいに分かれます。ある要素が別の要素に依存せず、大きなデータセットのすべての要素に同じ操作を適用する。このデータ並列性は最も親しみやすい種類で、競合する共有状態がほとんどなく、高速化がコア数に近づきえます。map-reduceのパイプライン、並列な配列操作、ベクトル化された数値コードがすべて示すとおりです。ここでもアムダールの法則を尊重してください。マージやリデュースのステップはしばしば直列で、利得に上限を課し、分割のオーバーヘッドが小さな入力では支配的になりえます。要素ごとの仕事が相当で、要素が本当に独立しているときに手を伸ばしてください。そうでなければ、最も単純な逐次版が、十分速く、かつ正しく保つのがはるかに容易なことが多く、それは2.9章の構築の実践が補強する点です。

非決定的なコードを意図してテストし、デバッグする

並行性のバグは非決定的なので、一つのインターリーブを実行する通常のテストは、ほとんど見逃します。ランダム化されたタイミングで多くのタスクを実行して、まれな順序を揺り出す、ストレステストとファズテストで、問題に意図して取り組みます。メモリアクセスを計装して、バグを含むインターリーブが今回起きなかったときでもデータ競合を捉える、競合検出器とスレッドサニタイザーに手を伸ばします。プラットフォームが提供する所では、特定のインターリーブを再生する決定的なシミュレーションや制御されたスケジューラを使い、ハイゼンバグを再現可能にし、本番のハングが、スレッドの状態とロックの所有者を捕捉できるよう設計します。それは2.15章のデバッグの規律につながります。何よりも、これらのバグのカテゴリ全体を不可能にする設計(不変性、メッセージパッシング、単一の所有)を好んでください。作れないバグは、デバッグする必要が決してないバグだからです。

トレードオフ: 長所と短所

アプローチ長所短所
ロックを伴う共有メモリ操作ごとに速い。馴染みがある競合、デッドロック、可視性のバグ。多くの作者が正しく保つのが難しい
不変性同期が不要。読み取りが自明にスレッドセーフコピーのコスト。大きな可変構造には不向き
メッセージパッシング(アクター、チャネル)明示的なデータフロー。バグの種類全体が消えるメッセージごとのオーバーヘッド。キューに上限がないとバックプレッシャーを隠しうる
async/awaitI/Oバウンドの仕事に安い並行性。逐次的に見えるコードCPUの仕事には並列性がない。タスクのブロックが他を止める
構造化された並行性明確なタスクの寿命。エラーが伝播する。タスクの漏れがない新しく、一部のエコシステムでは利用しにくい
データ並列性独立した仕事でほぼ線形の高速化アムダールの上限。小さな入力ではオーバーヘッドが支配的
アトミック / ロックフリー単純な値でロックの競合がない微妙に間違えるのが極めて容易。レビューが難しい

中心的な緊張は、安全性と生の速度であり、解決は、まず正しさを買い、測定が必要だと証明した所にだけ性能を使うことです。生の共有メモリのロックは、操作あたり最速で、コードの行あたり最も危険です。高水準のモデルは、わずかなスループットを要し、大きな安全性と明晰さを返し、多くの手で保守されるコードでは、その取引は決定的に行う価値があります。手で調整したロックフリーの並行性は、プロファイラ(2.16章)が調整のオーバーヘッドが重要だと証明する小さなホットスポットのために取っておき、それらも十分にテストされた境界の背後に保ちます。

チームで議論すべき問い

  1. 最も忙しいサービスのワークロードはI/OバウンドですかCPUバウンドですか。そして並行性の設計はそれに合っていますか。 チームは日常的に、時間の95%をデータベースの待機に費やすサービスにスレッドプールを加え、スループットを得ずに競合を得たり、直列の割合がどんな高速化にも上限を課す計算を並列化しようとしたりします。正しい設計は領域から導かれます。待機の多い仕事にはasyncか小さなプール、計算の重い仕事にはコアにわたる本物の並列性。時間が実際にどこに行くかを示すプロファイルを、仮定ではなく持ち込み、時間の大半が計算に費やされているなら、直列の割合を測定し、アムダールの法則に上限を教えさせてください。答えが、asyncか、上限付きのプールか、データ並列性のどれに手を伸ばすかを形づくります。

  2. タスク間で状態を共有するチームの既定は何で、それは構造上安全ですか。 大きなチームでは、例外より既定が重要です。ほとんどのコードは並行性の専門家でない人々によって書かれ、すでにあるパターンをそのままコピーするからです。既定が場当たり的なロックで守られた共有の可変オブジェクトなら、忘れられたロック一つ先に、数か月後に本番で表面化する競合があります。既定が不変性とメッセージパッシングなら、バグのカテゴリ全体が起きず、本当に共有メモリを必要とするまれな場所が、慎重なレビューのために際立ちます。新しいエンジニアが今日何に手を伸ばすか、レビューが同期されていない書き込みを捉えるか、そして安全な経路を簡単な経路にする方法を議論してください。

  3. 本番で百万リクエストに一度現れる並行性のバグを、どう見つけ、再現し、直しますか。 多くのチームにとっての正直な答えは、できないというものです。見ているとバグは消え、テストは一つの無害なインターリーブしか実行しないからです。それは心配すべきことです。これらのバグは静かにデータを壊し、信頼を蝕むからです。継続的インテグレーションで競合検出器とスレッドサニタイザーを動かしているか、ランダム化されたタイミングでストレステストしているか、本番のオブザーバビリティがハングの瞬間にスレッドとロックの状態を捕捉しているかを話し合ってください。最良のチームは、モデルの選択によってそのようなバグのほとんどを不可能にすることで答え、残る少数がまれで封じ込められるようにしています。

  4. システムのどこに制限のないキューや上限のないスレッドプールがまだ存在し、突然の十倍の急増のもとで、それはどうなりますか。 これが重要なのは、処理中の無制限な仕事が、謎のメモリ不足のクラッシュを装う失敗だからです。仕事が排出されるより速く到着し、メモリが埋まり、サービスは、それが実際には過負荷であるのに、ハードウェアの故障に見える形で死にます。相反する考慮は本物です。低すぎる上限は正当なトラフィックを拒否し、高すぎる上限はクラッシュを防ぐのではなく先送りするので、その数字は、推測ではなくキャパシティの決定です。すべてのキューとプールの一覧、それぞれの現在の上限(または上限がないことの自認)、満杯になったときのバックプレッシャーの振る舞い、縁でシステムがどう劣化するかの負荷テストの証拠を持ち込んでください。企業のフリートでは、一つの制限のないキューがフリート全体の障害に連鎖しえて、市民に対して可用性を保たなければならない政府のプラットフォームでは、明確なエラーを伴う穏やかな拒否がサービスの義務なので、上限とその拒否の経路は、一人のエンジニアの記憶ではなく、キャパシティ計画とランブックに属します。

  5. 高水準の並行性モデルと手書きのロックの使い分けについてのチームの方針は何で、どこで例外を認めましたか。 既定のモデルが、平均的な変更がどれだけ安全かを決めます。ほとんどの作者は並行性の専門家ではなく、すでにあるパターンをコピーするからです。アクター、チャネル、構造化された並行性は全員の床を引き上げ、生のロックは理論上は正しく、実践ではデッドロックの源です。緊張は、高水準のモデルがわずかなメッセージごと、タスクごとのオーバーヘッドを要し、プロファイラがときにホットパスが手で調整したロックフリーのコードを必要とすると証明するので、全面的な禁止は自由放任と同じくらい間違っていることです。安全な既定より下に手を伸ばした場所のリスト、それぞれを正当化したプロファイリングの証拠、各例外がテストされた境界と文書化されたロックの順序の背後にどう囲い込まれているかを持ち込んでください。大企業では、この方針が、何千人もの貢献者がそれぞれ安全でない方式を再発明するのを防ぎ、長寿命の政府のシステムでは、何年も後のレビュアーが、なぜ危険なパターンが認められたかを理解し、それがまだ必要かを確認できるようにするものです。

  6. 計算の並列化を決めたとき、直列の割合をどう測定し、高速化が本物であることを確認する責任は誰にありますか。 チームは日常的に、計算をコアに分散させ、プロファイラが決して確認しない数字を祝います。アムダールの法則は、コアをいくら増やしても利得を直列の割合の逆数に制限し、分割とマージのオーバーヘッドは、小さな入力では利益を完全に消しうるからです。相反する引力は、並列性が本物の複雑さと新しい競合の表面を加えることなので、問いは、測定された高速化が、引き受ける正しさのリスクを正当化するかどうかです。直列の部分を切り出したプロファイル、並列性が実際に勝つ入力サイズ、希望的な見積りではなく代表的なハードウェアでの前後のベンチマークを持ち込んでください。大きな計算フリートに支払う企業にとって、誠実な直列の割合の分析は、節約された、あるいは無駄にされたハードウェア支出になり、公共システムのコストに説明責任のある政府機関にとって、並列設計を承認した人は、監査のもとでそれを正当化した測定を示せるべきです。

セクター別の視点

スタートアップ。 小さなチームで余裕のあるランウェイもないので、雇えない並行性の専門家ではなく、構造で正しさを買ってください。言語が与える単一の安全な既定に手を伸ばします。I/Oバウンドの仕事にはasync/await、共有状態には一つの所有するタスクかアクター、そして手で調整するロックは完全に省きます。決済の経路での失われた更新の競合は、見逃した機能よりも速くあなたを沈めうるので、その種類のバグを不可能にするために少しの余分なコードを使い、先へ進みます。

小規模事業者。 並行性が仕事の人はいないので、代わりにそれを扱ってくれるプラットフォームとマネージドサービスを好みます。データベースのトランザクション、ホスト型のキュー、フレームワークのリクエストモデルは、手で保守するスレッドに勝ります。ツールを評価するとき、「これは並行性を既定で安全にするか」を、作るか買うかの問いとして扱い、間違ったインターリーブが顧客のレコードを静かに壊せない選択肢を好みます。上限のあるマネージドサービスに保持させられる所では、自分のコードから共有された可変状態を排除してください。

大企業。 多くのチームにわたって、目標は何千人もの貢献者を安全に保つハウスの既定です。不変性とメッセージパッシングを標準とし、生のロックより高水準のモデル、バックプレッシャーを伴う上限付きのキューとプール、文書化されたグローバルなロックの順序。これらをエンジニアリング標準に符号化し、CIの競合検出器とストレステストで徹底し、プロファイラがロックフリーのコードを正当化した例外を統治して、それぞれがテストされレビューされた境界の背後に留まるようにします。並行性のキャパシティを、測定された負荷に結びついたキューの上限とプールのサイズを伴う、フリート全体の関心事として管理します。

政府。 何十年も動くシステムでの正しさと監査可能性は、生のスループットに勝ります。疑われる競合を再現して、修正を監督機関に証明できるよう、あらゆる状態遷移が記録され再生可能であることを求め、給付、安全、公的記録に触れる決定には、AIを使わない決定的な経路を保ちます。調達では、ベンダーに並行性モデルの開示と、競合検出器とストレステストのカバレッジの証拠を求めてください。公共のシステムの負荷のもとでの間違った答えは、不便ではなく、責任ある職員が後に説明しなければならないものだからです。

事例

スタートアップ。 小さなチームが決済機能を出荷し、アカウントの残高が負荷のもとでときどき数セントずれることに気づきます。原因は、並行するリクエストハンドラからの残高フィールドに対する単純な読み取り・変更・書き込み、失われた更新の競合です。ロックをばらまく代わりに、各アカウントの残高を、借方と貸方をメッセージとして一度に一つずつ処理する単一の所有するタスクの背後に移します。ずれは消え、コードは推論しやすくなり、修正を守るために、何千もの並行する送金を発射するストレステストを加えます。一つの構造的な変更で、バグの種類全体が退役しました。

大企業。 毎秒数万のリクエストを扱う高スループットの注文サービスが、トラフィックの急増の間、周期的なレイテンシの急上昇とときどきのメモリ不足のクラッシュに苦しみます。調査で、需要が容量を超えると際限なく伸びる、スレッドプールの背後にある制限のない作業キューが見つかります。チームはキューに上限を設け、プールをコア数に結びついたサイズに制限し、超過した負荷を明確なエラーで素早く拒否するバックプレッシャーを加えます。スループットは予測可能になり、クラッシュは止まり、共有キャッシュのホットなロックは、プロファイラが競合が本物だと証明した後にだけ、ロックフリーの構造に置き換えられます。多くの作者のための安全な既定と、測定された所だけの調整された並行性。

政府。 国の給付プラットフォームは何十年も稼働し、並行するケースの更新のもとでも監査可能で正しい結果を出さなければなりません。チームは、ハウスの既定として不変性とメッセージパッシングを選び、すべての可変状態を単一の所有者に閉じ込め、ロックが残る所ではグローバルなロックの順序を課し、そのすべてをエンジニアリング標準に書き込みます。パイプラインでスレッドサニタイザーとランダム化されたストレステストを実行し、すべての状態遷移が監督のために記録され再生可能になるよう設計するので、まれなインターリーブが疑われるとき、再現して修正を証明できます。正しさと監査可能性は、性能の後付けではなく、第一級の要件として扱われます。

ビジネスケース: 動機、ROI、TCO

規律ある並行性の見返りは、起こらないインシデントとして現れます。本番の競合一つが、何千ものレコードにわたってデータを壊しえて、コストには、観察されると隠れるバグを見つけるエンジニアリングの時間と、悪いデータを照合し、影響を受けたユーザーに通知し、信頼を再構築するはるかに大きなコストの両方が含まれます。これらは、まさに非決定的であるために、診断するのが最も高価な欠陥の一つなので、一つのハイゼンバグを追うことは、前もって安全なモデルを選ぶ労力をはるかに上回りえます。

上振れはスループットとコストにも現れます。並行性を適切にサイズ決定することで、サービスは同じハードウェアではるかに多くの負荷を扱え、それは大きなフリートにとって繰り返し発生する節約となり、バックプレッシャーと上限付きのキューは、トラフィックの急増を公のインシデントに変える連鎖的な障害を防ぎます。総所有コストはささやかで、おもに文化的です。ハウスのスタイル(不変性、メッセージパッシング、構造化された並行性)、ツール(CIの競合検出器、スレッドサニタイザー、ストレスハーネス)、そしてロックの順序と上限を符号化する標準に投資します。代替は、正しさがすべての作者が永遠に専門家であることに依存するコードベースであり、成長するチームには維持できません。リーダーシップにはその言葉で論拠を示してください。防がれた競合を避けられたデータ破損のインシデントに、バックプレッシャーを防がれた障害に、安全な既定を節約されたオンボーディング時間に翻訳します。

アンチパターンと落とし穴

  • I/Oバウンドの仕事に、速度のためにスレッドを加える。 待機の多いサービスのスレッドを増やしても、スループットではなく競合を買うだけです。
  • あらゆる所に共有された可変状態。 どのスレッドがどのオブジェクトも変更するなら、正しさは、どのレビュアーも検証できない運の問題になります。
  • 制限のないキューとプール。 急増がメモリが死ぬまでキューを伸ばします。クラッシュは謎に見えますが、単純な過負荷です。
  • グローバルな順序のない場当たり的なロック。 コードベースの各所で異なる順序で取られたロックが、負荷のもとでデッドロックします。
  • コードが書いた順序で動くと想定する。 メモリモデルを無視し、可視性のバグが、古い値の上でスレッドを空回りさせたままにすること。
  • 手作りのロックフリーの巧妙さ。 独自のロックフリーの方式は、ほぼ常に微妙に間違っていて、レビューがほぼ不可能です。
  • 幸せなインターリーブだけをテストする。 決定的なテストは合格する間に、百万回に一度の順序が本番を壊します。
  • ロックを持ったまま未知のコードを呼ぶ。 ブロックしたり再入したりするコールバックが、クリティカルセクションをデッドロックに変えます。

成熟度モデル

  • レベル1、開始: 並行性は場当たり的で反応的です。スレッドとロックは本能で加えられ、共有された可変状態があらゆる所にあり、キューには上限がありません。競合状態は誰にも診断できない再現不能な本番インシデントとして表面化し、それを捉えるツールもありません。
  • レベル2、発展: 一部のチームは基本的な実践を学びました。より注意深くロックを使い、最も明らかなキューに上限を設けます。競合とデッドロックへの非公式な認識があり、いくつかの重要な経路は追加の精査を受けます。実践はチーム間で一貫せず、テストはまだ大半が単一のインターリーブで、安全なパターンは文書ではなく個人に宿っています。
  • レベル3、標準化: 組織は、組織全体で徹底される文書化されたハウスのスタイルを持ちます。既定としての不変性とメッセージパッシング、生のロックより高水準のモデル、バックプレッシャーを伴う上限付きのキューとプール、文書化されたグローバルなロックの順序。競合検出器とストレステストがCIで動き、並行性の選択は、仕事がI/OバウンドかCPUバウンドかから導かれます。
  • レベル4、管理: 組織は並行性の姿勢をベースラインに対して測定し、制御します。サービス全体での競合検出器とスレッドサニタイザーのカバレッジを追跡し、キューの深さ、ロックの待ち時間、プールの飽和、拒否率を監視される指標として記録し、劣化の曲線を負荷テストして、あらゆる上限をデータに裏付けられたキャパシティの決定にします。並行性のインシデントは数えられて傾向が見られ、並列化されたワークロードの直列の割合は、実際に達成された高速化に対して測定され、新しい設計の継続・中止の判断は、本能ではなくその証拠に基づきます。
  • レベル5、オーケストレーション: 安全な並行性は、すべての作者にとって最も抵抗の少ない道であり、実践は継続的に改善され、組織全体に統合されています。バグの種類全体が構造上不可能で、ホットスポットはプロファイリングがそれを証明した所でのみ調整され、決定的な再生がまれな残りのバグを再現可能にします。正しさと監査可能性は継続的に守られる特性で、キャパシティの上限は観察された負荷に適応し、標準はプラットフォームとワークロードの変化に応じて進化します。

議論のためのアイデア

  1. 今日、最も忙しいサービスを監査したら、その状態のどれだけが共有され可変で、その共有のどれだけが本当に必要ですか。
  2. 誰かが二つのタスクを調整する必要があるとき、チームの既定の答えは何で、それが不変性かメッセージパッシングであってほしいですか。
  3. システムのどこに制限のないキューや上限のないプールがまだ隠れていて、突然の十倍のトラフィックの急増のもとで何が起こりますか。
  4. 継続的インテグレーションの実行には競合検出器やスレッドサニタイザーが含まれていますか。そして最後にそれが本番の前に何かを捉えたのはいつですか。
  5. 最も並列化されたワークロードの直列の割合はどれくらいで、アムダールの法則は、実際に追っている高速化に上限を課しますか。
  6. チームは百万回に一度のインターリーブのバグを要求に応じて再現できますか。そしてそこに至るには何が必要ですか。

要点

  • 並行性はプログラムを独立したタスクとして構造化し、並列性はそれらを同時に実行します。スレッドを加える前に、どちらが必要かを決めます。
  • 共有された可変状態は、ほぼすべての並行性のバグの根源です。多くの作者のための安全な既定として、不変性とメッセージパッシングを好みます。
  • 理論上は正しく実践では危険な手書きのロックの前に、高水準のモデル(アクター、チャネル、構造化された並行性、async/await)に手を伸ばします。
  • 原子性、可視性、メモリモデルを理解します。適切なプリミティブを使い、ロックを短く保ち、デッドロック、ライブロック、飢餓を避けるためにグローバルなロックの順序を課します。
  • すべてのキューとプールに上限を設けてバックプレッシャーを適用し、急増がクラッシュではなく穏やかに劣化するようにします(3.3章)。
  • 競合検出器、ストレステスト、再生でインターリーブを意図してテストし(2.15章)、並列化するときはアムダールの法則を尊重します(2.16章)。
  • 企業にとってこれはスループットと防がれたインシデントであり、政府にとっては長寿命のシステムにおける正しさと監査可能性です。

参考文献とさらなる読み物

  • Brian Goetz et al., Java Concurrency in Practice (atomicity, visibility, the memory model, and safe publication).
  • Herb Sutter, “The Free Lunch Is Over” (why software must embrace concurrency as clock speeds plateau).
  • Leslie Lamport, “Time, Clocks, and the Ordering of Events in a Distributed System” (ordering and the foundations of concurrent reasoning).
  • C. A. R. Hoare, “Communicating Sequential Processes” (Communications of the ACM, 1978): the CSP model behind channels.
  • Carl Hewitt, Peter Bishop, and Richard Steiger, “A Universal Modular Actor Formalism for Artificial Intelligence” (the origin of the actor model).
  • Edsger W. Dijkstra, “Cooperating Sequential Processes” (semaphores, mutual exclusion, and the deadlock problem).
  • Maurice Herlihy and Nir Shavit, The Art of Multiprocessor Programming (locks, atomics, and lock-free data structures).
  • Nathaniel J. Smith, “Notes on Structured Concurrency, or: Go Statement Considered Harmful” (the case for structured concurrency).
  • Martin Kleppmann, Designing Data-Intensive Applications (concurrency and consistency where memory meets distributed systems).
  • Gene M. Amdahl, “Validity of the Single Processor Approach to Achieving Large-Scale Computing Capabilities” (1967): the origin of Amdahl’s law.