代表的な面接トピック

プロセスとスレッドの違いとは何か?

一般普通
Offer.cc 編集チーム公開日 更新日

質問

あるアプリケーションで、クラッシュやハングの可能性があるCPUバウンドなサードパーティ製プラグイン8個を実行し、同時に大半が読み取り専用の大規模キャッシュを共有する最大2,000件のI/Oバウンドな並行リクエストを処理する必要があります。各ワークロードに対してプロセス、スレッド、または別の並行性モデルのどれを採用しますか?その理由も説明してください。

質問とそれを使用する場面

あるアプリケーションで、クラッシュやハングの可能性があるCPUバウンドなサードパーティ製プラグイン8個を実行し、同時に大半が読み取り専用の大規模キャッシュを共有する最大2,000件のI/Oバウンドな並行リクエストを処理する必要があります。プロセスとスレッドの違いを説明した上で、各ワークロードに適した実行モデルを選択してください。リソースの所有権、スケジューリング、通信、同期、障害およびセキュリティの分離、ライフサイクル、検証について網羅してください。

これは、ソフトウェア、バックエンド、インフラストラクチャ、システム系の職種におけるオペレーティングシステムの基礎に関する質問です。「8」や「2,000」という数値は面接用の前提条件であり、普遍的なサイジングルールではありません。1つ目のワークロードは分離性とCPUの並列性を重視し、2つ目は高いI/O並行性と共通データへの効率的なアクセスを重視します。有益な回答とは、プロセスまたはスレッドのどちらかが普遍的に高速であると断定するのではなく、これらの制約から2つの選択を導き出すものです。

面接官が評価しているポイント

第1の評価ポイントは、正確な所有権モデルを理解しているかです。プロセスとは、仮想アドレス空間、実行可能コード、開かれたシステムリソース、セキュリティコンテキスト、および少なくとも1つのスレッドを持つ、リソースと分離のコンテナです。スレッドとは、そのプロセス内にあるスケジューリング可能な実行コンテキストです。1つのプロセス内の複数のスレッドはそのアドレス空間とプロセス全体のリソースを共有しますが、各スレッドはレジスタ、スタック、スレッド識別子、スレッドローカルストレージなどの実行状態を個別に保持します。

第2の評価ポイントは、並行性(concurrency)と並列性(parallelism)を区別できているかです。単一のコア上で複数のタスクがインターリーブ(交互実行)することにより、並行して処理を進めることができます。これらが並列に実行されるのは、ランタイムとオペレーティングシステムが複数のコア上でそれらを実行する場合のみです。2,000個のスレッドを作成しても2,000通りのCPU並列処理が実現するわけではなく、8個のCPUバウンドなジョブがあるからといって、マシンのCPUおよびメモリの予算内に8個のワーカープロセスが収まることを意味するわけでもありません。

第3の評価ポイントは、エンジニアリングとしての判断力です。共有メモリによりスレッド間通信は直接的になりますが、競合状態、ロックの競合、プロセス全体に及ぶ障害リスクが生じます。プロセスを分離すると、デフォルトでより強固なフォールトおよびメモリの境界が提供されますが、IPC、監視、シリアライズまたは共有メモリのプロトコルが必要になります。プロセス分離単体では、信頼できないコードに対する完全なサンドボックスにはなりません。権限、システムコール、ファイル、ネットワークアクセス、CPU、メモリにも制限が必要です。

回答前に明確にすべき質問

  • 「サードパーティ」とは何を意味するか? 信頼できるがバグのあるコードであれば、主にクラッシュやハングの分離が必要です。敵対的なコードの場合は、適切なサンドボックス、最小権限、リソース制御も必要になります。
  • プラグインは大容量のモデルやキャッシュを共有する必要があるか? メモリが独立していればプロセスが有利になります。非常に大規模な読み取り専用データセットの場合、ワーカーごとに物理コピーを1つずつ作成するのを避けるために、共有の読み取り専用マッピングが必要になる場合があります。
  • 言語ランタイムはCPUバウンドなスレッドの並列実行を許可しているか? ネイティブスレッドは複数コアを使用できますが、ランタイムのロックやスケジューラがアプリケーションコードをシリアライズする場合があり、選択肢が変わることがあります。
  • リクエストハンドラーはブロッキングライブラリとノンブロッキングライブラリのどちらを使用しているか? ブロッキングの依存関係には、サイズ制限されたスレッドプールが適しています。完全にノンブロッキングなスタックであれば、イベントループと少数のOSスレッドで多くの待機接続を処理できます。
  • キャッシュはイミュータブルまたはバージョン管理可能か? アトミックな置き換えを伴うイミュータブルなスナップショットは、きめ細かなロックを必要とするミュータブルなオブジェクトグラフよりも安全に共有しやすくなります。
  • 障害およびレイテンシの目標は何か? プラグインのデッドライン、再起動の予算、リクエストのp99、キャンセル規約、メモリ制限、過負荷ポリシーによって、プールサイズやキューの制限が決まります。

30秒の回答フレームワーク

「プロセスは分離された仮想アドレス空間とプロセス全体のリソースを所有し、1つ以上のスレッドを含みます。スレッドはプロセスの状態を共有しつつ、自身のスタック、レジスタ、識別子、スレッドローカル状態を保持するスケジューリング可能な実行コンテキストです。クラッシュしやすいCPUプラグインは、障害やハングが発生してもホストのヒープを共有せずに強制終了して置き換えられるよう、監視付きのリソース制限されたプロセスで実行します。ワーカー数は単に8とするのではなく、CPUとメモリの予算に応じて決定します。大半が待機状態となる2,000件のリクエストに対しては、依存関係全体がノンブロッキングであれば非同期イベントループを使用し、ライブラリがブロックする場合はサイズ制限されたスレッドプールを使用します。スレッドは大半が読み取り専用のキャッシュを、好ましくはイミュータブルなスナップショットとして共有できます。決定を下す前に、スループット、p99、メモリ、コンテキストスイッチ、IPCまたはロックの待機時間をベンチマークし、クラッシュ、ハング、競合状態を注入してテストします。」

ステップごとの解決策

ステップ 1: 所有権モデルを構築する

移植性のあるメンタルモデルは、プロセスを「リソースの境界」、スレッドを「その中の実行フロー」として捉えることです。カーネルの正確な実装はプラットフォームによって異なるため、単一のオペレーティングシステムの内部オブジェクトモデルを普遍的なものとして提示しないようにしてください。

状態またはリソースプロセスとの関係スレッドとの関係
仮想アドレス空間、コード、ヒーププロセス間ではデフォルトで分離同一プロセス内のスレッド間で共有
開かれたファイルおよびその他のプロセスリソースプロセスが所有または参照。継承や明示的な共有が可能通常、プロセス内のスレッド間で共有
スタック、レジスタ、プログラムカウンタプロセスはスレッドを通じてこれらを保持スレッドごとに独立
スレッドローカルストレージとスレッドIDプロセス全体で1つの値ではないスレッドごとに独立
セキュリティとリソース制限分離ポリシーを適用する自然な単位主にプロセス全体単位。プラットフォームによっては偽装(impersonation)などスレッド単位の詳細をサポート

「デフォルトで分離されている」という点が重要です。プロセス間でも意図的にメモリ、ファイル、ハンドルを共有することができ、スレッド間でも任意の共有ミューテーションの代わりにキューを介して通信することができます。この選択によって定まるのはデフォルトの障害および所有権の境界であり、利用可能な通信APIが唯一に限定されるわけではありません。

ステップ 2: 並行性、並列性、コストを分離して考える

並行性とは、複数の作業単位が進行中であることを意味します。並列性とは、異なる処理リソース上でまったく同じ瞬間に作業が実行されることを意味します。単一スレッドでも多数の非同期I/O操作を多重化できます。複数の実行可能なスレッドやプロセスは複数のコアを使用できます。実際のCPU並列性は、利用可能なコア数、コンテナのクォータ、ランタイムの動作によって制限されます。

一般に、スレッドは単一のアドレス空間を再利用するため作成や切り替えのコストが低く、プロセスはより多くのメモリとライフサイクル状態を保持します。これは一般的な傾向であり、パフォーマンスの保証ではありません。CoW(コピーオンライト)によるプロセス作成、スレッドスタックの事前確保、キャッシュミス、アドレス空間の切り替え、ランタイムのスケジューリング、IPCペイロード、ロック競合などによって、特定のワークロードにおける支配的なコストが逆転することもあります。特定のマイクロ秒数やメモリ数値を普遍的なものとして当てはめるのではなく、対象のランタイムとプラットフォームで測定してください。

ステップ 3: 通信と正確性のコストを比較する

スレッドは共有データへのポインタを渡すことができますが、すべてのミュータブルなオブジェクトには所有権または同期のルールが必要です。1つのキャッシュエントリに対して2つのスレッドが読み取り・変更・書き込みを行うと、各ソースコードの行が単純に見えても更新が失われる可能性があります。ロック、アトミック操作、イミュータブルデータ、メッセージパッシング、所有権のパーティショニングなどは、それぞれ異なるアクセスパターンを解決します。正確性を維持するロックであっても、競合が発生すると長いキューイングやp99レイテンシの悪化を引き起こす可能性があります。

プロセスは通常、パイプ、ソケット、キュー、またはRPCを介してメッセージを交換します。これにより明示的なプロトコルが作られ、所有権の監査が容易になりますが、シリアライズ、コピー、バックプレッシャー、部分障害処理のコストがかかります。共有メモリを使用するとコピーを排除できますが、その場合はプロセス間でバージョン管理と同期のプロトコルが再び必要になります。IPCは並行性のバグを排除するのではなく、メッセージの識別性、順序付け、リトライ、タイムアウト、ライフサイクルの境界へと移動させるだけです。

ステップ 4: プラグインワークロードに監視付きプロセスを選択する

8個のCPUバウンドなサードパーティ製ジョブには、ホストによって監視されるサイズ制限付きのプロセスワーカープールを使用します。各ジョブに識別子、入力規約、デッドライン、出力規約、キャンセル動作を割り当てます。終了したワーカー、デッドラインを超過したワーカー、リソース制限に違反したワーカーは終了させられて置き換えられます。スーパーバイザーはそのジョブを安全に再試行できるかどうかを判断します。プラグインの状態はホストのヒープから排除し、明示的な入力と出力を渡します。

プールサイズは、CPUクォータ、プラグインのメモリ、サービスの余力(ヘッドルーム)から決定します。4コアのクォータにおいて、常に実行可能なワーカーを8個起動すると、全体のCPU作業量を減らすことなくコンテキストスイッチを増加させる可能性があります。プラグインが大規模な共通の読み取り専用データセットを必要とする場合は、検証済みの読み取り専用スナップショットをワーカーにマッピングするか、専用のデータサービスを実行します。単にコピーの発生を避けるという理由だけで障害の分離を放棄してはなりません。

独立したプロセスは、セキュリティの1つの層に過ぎません。悪意を持つ可能性のあるプラグインには、制限されたID、サンドボックスやコンテナの境界、(利用可能な場合は)許可されたシステムコールポリシー、ファイルシステムおよびネットワークの制限、CPUとメモリのクォータ、出力バリデータが必要です。また、ワーカーのログ、クラッシュダンプ、再起動の試行が大量に発生してスーパーバイザーを圧迫しないように隔離します。

ステップ 5: リクエストに対して非同期I/Oまたはサイズ制限されたスレッドプールを選択する

大半の時間を待機に費やす最大2,000件の並行リクエストに対して、「1リクエスト」を「1つの新規プロセス」に直接マッピングしてはなりません。ネットワーク、データベース、クライアントライブラリがエンドツーエンドでノンブロッキングである場合、イベントループによって少数のスレッド上で多数のリクエストを処理中(in flight)の状態に維持できます。CPU負荷の高い作業はイベントループから逃がす必要があり、過負荷時にメモリが無制限に増大する代わりにリクエストの拒否やバックプレッシャーとして現れるよう、すべてのキューに上限を設ける必要があります。

必要なライブラリがブロックする場合は、その依存関係に合わせてサイズを設定・測定した制限付きスレッドプールを使用します。この上限により、メモリ、開かれた接続、ダウンストリームのキャパシティが保護されます。スレッドはIPCなしで読み取り中心のキャッシュにアクセスできます。可能であれば、アトミックな参照を通じてイミュータブルでバージョン管理されたスナップショットを公開します。ミューテーションが避けられない場合は、ロックのスコープを定義し、競合を測定します。ソケット用のイベントループと、ブロッキング作業用の制限付きプールを組み合わせるハイブリッドな構成の方が、単に「スレッド」か「非同期」かを選択するよりも適切な場合が多くあります。

ステップ 6: 測定とフォールト注入によって決定を検証する

本番環境に近いペイロードと待機比率を用いて、同一のマシンまたはクォータ上で両方の候補をベンチマークします。スループット、p50およびp99レイテンシ、CPU使用率、常駐メモリおよびプロポーショナルメモリ、キュー待機時間、コンテキストスイッチ、プロセス構成におけるIPCバイト数とシリアライズ時間、スレッドまたは非同期構成におけるロック待機時間とイベントループの遅延を記録します。結果を比較する際は、ウォームアップ、入力分布、ワーカー数、キューの制限を同一にする必要があります。

正常系だけでなく、境界条件も徹底的にテストします:

  1. プラグインをクラッシュさせ、ホストおよび他のワーカーが利用可能な状態を維持していること、終了が検知されること、再起動ポリシーが制限内であることを検証する。
  2. プラグインをハングさせ、デッドライン、終了処理、クリーンアップ、リトライの判断を検証する。
  3. プラグインのメモリとログを強制的に増大させ、クォータによってホストが保護されることを検証する。
  4. キャッシュの並行読み取りと更新に負荷をかけ、ランタイムに競合検出機構(race detector)がある場合はそれを使用し、リーダーが完全な古いスナップショットまたは新しいスナップショットのいずれかを参照できていることを検証する。
  5. リクエスト処理を飽和させ、制限付きキュー、キャンセル、ダウンストリームの制限、過負荷時の応答を検証する。
  6. サービスを再起動し、処理中のタスクの所有権が規約に従って回復されるか適切に失敗として処理されるかを検証する。

再利用可能な決定基準は次のとおりです。分離性、独立したライフサイクル、またはランタイムのCPU並列性が支配的な場合はプロセスの境界を選択します。同一プロセス内の共通状態への低コストなアクセスが支配的で同期が扱いやすい場合は共有スレッドを選択します。待機の並行性が支配的で依存関係チェーンがノンブロッキングなキャンセルをサポートしている場合は非同期タスクを選択します。ピーク時のスループットだけでなく、障害が発生し得る境界を検証してください。

優れた回答の例

「所有権の観点から説明します。プロセスは独自の仮想アドレス空間とプロセス全体のリソースを持ち、少なくとも1つのスレッドを含みます。そのプロセス内のスレッドはヒープや開かれたリソースを共有しますが、各スレッドは独自のスタック、レジスタ、識別子、スレッドローカル状態を持ちます。これによりスレッドはデータの共有に便利ですが、不正な書き込みや致命的な障害がプロセス全体に影響を及ぼす可能性があります。プロセスは通信をより明示的にし、デフォルトでより強固な障害境界を提供しますが、共有メモリや継承されたリソースを使用することでその境界を設定変更することも可能です。

8個のCPUバウンドなプラグインに対しては、監視付きのワーカープロセスを使用します。スーパーバイザーはIDとデッドラインを付与してジョブを送信し、結果を検証し、プロセスの終了を監視し、再起動の予算内で障害が発生したワーカーを置き換えます。ワーカー数はCPUクォータとメモリの測定結果に基づいて決定し、8個のジョブがあるからといって自動的に8個のワーカーにするわけではありません。プラグインが信頼できないものである場合、プロセスを分離するだけでは不十分なため、権限、システムコール、ファイル、ネットワーク、CPU、メモリも制限します。

主にI/O待機となる2,000件のリクエストに対しては、使用するライブラリを調査します。ノンブロッキングなパスであればイベントループを使用し、CPU処理は制限付きプールに逃がします。ブロッキングな依存関係がある場合は、サイズ制限付きのスレッドプールを使用します。大半が読み取り専用のキャッシュは、アトミックに公開されるイミュータブルでバージョン管理されたスナップショットとし、読み取りごとのロックを回避します。すべてのキューとダウンストリーム呼び出しには、デッドラインと容量制限を設定します。

スループットとp99を、CPU、メモリ、キュー時間、コンテキストスイッチ、IPCまたはロックの待機時間、イベントループの遅延とともに比較します。さらに、プラグインのクラッシュ、ハング、メモリ負荷をテストし、キャッシュ更新時の競合を発生させます。一般にスレッドやプロセスが軽量と言われているからではなく、これらの障害テストを通過して分離性と正確性の主張が実証された場合にのみ、その設計を採用します。」

よくある間違い

  • プロセスをプログラム、スレッドを関数と表現する → リソースの所有権とスケジューリング可能な状態の説明が抜けています → プロセスのアドレス空間の境界と、スレッドの共有状態およびプライベートな実行状態を説明してください。
  • スレッドはすべてを共有すると主張する → 各スレッドは独自のスタック、レジスタ、識別子、スレッドローカル状態を持ちます → プロセス全体の状態とスレッドごとの状態を分けてリストアップしてください。
  • プロセスはメモリを共有できないと主張する → 明示的な共有マッピングは可能です → プロセスはデフォルトで分離されていると述べ、安全に共有するために必要なプロトコルを説明してください。
  • 並行性と並列性を同義語として扱う → 単一コア上でも作業を交互に実行できますが、同時に実行されているわけではありません → 並列性をコア数、クォータ、ランタイムの動作と関連付けて説明してください。
  • ジョブが8個あるからという理由で8個のワーカーを選択する → 実行可能なワーカーは有限のCPUとメモリを奪い合います → クォータ、測定結果、サービスの余力に基づいてプールのサイズを決定してください。
  • プロセスを信頼できないコードに対する完全なサンドボックスとして扱う → プロセスであっても、許可されたファイル、ネットワーク、カーネルインターフェースへのアクセスやリソースの枯渇が可能です → 最小権限、サンドボックスポリシー、クォータ、出力検証を追加してください。
  • 制限を設けずに待機リクエストごとに1つのスレッドを作成する → スタックメモリ、スケジューリング、ダウンストリーム呼び出しによってサービスが枯渇する可能性があります → 非同期I/O、またはバックプレッシャーを備えた測定済みの制限付きプールを使用してください。
  • 所有権ルールなしでミュータブルなキャッシュを共有する → データ競合やロック競合によって正確性やテールレイテンシが損なわれる可能性があります → イミュータブルなスナップショットを優先するか、同期の仕組みを定義してテストしてください。
  • 平均スループットのみを比較する → 設計によってはp99のキューイング、メモリの増大、脆弱な分離性が隠れてしまうことがあります → レイテンシ分布を測定し、クラッシュ、ハング、飽和状態、競合状態を注入してください。
  • スレッドは常に高速であると思い込む → ランタイム、IPC、キャッシュ、ロック、ワークロードのコストは状況によって異なります → オーバーヘッドの低さは仮説として扱い、実際の実装でベンチマークを行ってください。

フォローアップの質問と回答

フォローアップ 1: プロセス間で20GBの読み取り専用モデルを共有することは可能ですか?

はい、可能です。検証済みのイミュータブルなファイルまたは共有メモリ領域を各ワーカーに読み取り専用でマッピングすることで、OSがサポートしている環境では物理ページを共有できます。インプレースで変更するのではなく、マッピングをバージョン管理してワーカーを新しいスナップショットに切り替えます。ページフォールトとメモリ常駐の動作を測定し、リクエストごとのミュータブルな状態は共有領域の外に保持します。

フォローアップ 2: 言語ランタイムがCPUバウンドなスレッドをシリアライズする場合はどう変わりますか?

正確なランタイムとワークロードを確認してください。ロックがマネージド言語の実行のみを対象とし、ネイティブライブラリの実行時には解放される場合もあります。CPU処理が完全にシリアライズされる場合は、ワーカープロセスや、真の並列実行を提供するランタイム機能を使用します。ワーカープリミティブを変更しても過負荷自体は解決しないため、制限付きキューとキャンセル機能は維持してください。

フォローアップ 3: 1つのブロックされたスレッドによってプロセス全体が停止しますか?

通常、他の実行可能なスレッドは処理を継続できます。ただし、ブロックされたスレッドがロックを保持している、必要なイベントループを専有している、共有プールを枯渇させている、またはプロセス全体の初期化パス内で待機している場合は、プロセス全体が停滞する可能性があります。1つのスレッドのブロックを即座にプロセスのブロックと同一視するのではなく、依存関係と所有権のグラフを診断してください。

フォローアップ 4: スレッドプールがイベントループより優れているのはどのような場合ですか?

スレッドプールは、ブロッキングライブラリを使用する場合、並行度がそれほど高くない場合、およびコードのシンプルさが測定されたスレッドコストを上回る場合に適しています。イベントループは、すべての重要な依存関係がノンブロッキング操作とキャンセルをサポートしており、大規模な待機並行性がある場合に適しています。ソケット用にイベントループを使用し、不可避なブロッキング作業用に制限付きプールを使用するハイブリッドアプローチもあり、そのキューとデッドラインは設計の一部となります。

フォローアップ 5: 1つのワーカーのクラッシュが再起動ストームを引き起こすのを防ぐにはどうすればよいですか?

終了理由を分類し、一定の時間枠内での再起動回数に上限を設け、バックオフを追加し、繰り返し失敗するプラグインのバージョンを隔離し、キャパシティが安全でない場合は新規受け付けを停止します。中断されたジョブが再試行可能かどうかを判断できるよう、十分なジョブ状態を永続化します。スーパーバイザーを介して無制限のログやクラッシュダンプを送信することなく、クラッシュループを検知してアラートを発報します。

フォローアップ 6: ローカルプロセスの代わりに独立したサービスを採用すべきなのはどのような場合ですか?

ワーカーに独立したデプロイ、スケーリング、所有権、言語ランタイム、またはホストレベルのセキュリティポリシーが必要な場合は、サービスの境界を使用します。その場合のコストとして、ネットワークRPC、バージョン管理された規約、サービスディスカバリ、分散トレーシング、部分障害への対応が発生します。単一のホストレベルのスーパーバイザーとローカルIPCで分離とスケーリングの要件を満たせる場合は、ローカルプロセスのほうがシンプルなまま維持できます。

公開情報ソース

関連する質問