プロセスとスレッドの違い — メモリ共有がすべてを分けます

読了 5分

面接の定番の質問であり、サーバーの設定ファイルで毎日目にする実戦の概念です。nginx の worker_processes、gunicorn の --workers--threads、Java のスレッドプールが全部この概念の上にあります。プロセスとスレッドの違いを知っているかは、結局これらの設定値が何を変えるのか知っているかという質問です。違いの根は一つです。メモリを共有するか。 この記事はその一つの基準から残りの違いをすべて引き出します。

定義 — 実行の単位と、その中の流れ #

プロセスは実行中のプログラム一つで、OS がリソースを配分する単位です。プロセスごとに自分だけの仮想メモリ空間(コード、ヒープ、スタック)、ファイルディスクリプタ、権限を持ちます。互いのメモリは見えもせず、触れもしません。この隔離はカーネルとハードウェア(MMU)が強制します。

スレッドは一つのプロセスの中の実行の流れです。同じプロセスのスレッドはコード、ヒープ、ファイルディスクリプタを共有し、各自のスタックとレジスタだけを別に持ちます。カーネルのスケジューラが CPU に載せる単位はスレッドなので、「実行の単位はスレッド、リソースの単位はプロセス」という要約が成り立ちます。

共有が分ける三つの実務の違い #

第一に、通信のコスト — スレッド同士は同じヒープの変数をそのまま読み書きすれば済みます。速いのですが、同時に同じデータに触れた瞬間に競合状態が生まれるので、ロックが必要になります。マルチスレッドプログラミングの難しさ(デッドロック、レース)はすべてこの共有から来ます。プロセス同士はメモリが隔離されているので、パイプ、ソケット、共有メモリセグメントのような IPC を明示的に使う必要があります。遅くて面倒ですが、そもそも誤って互いのデータを壊しようがありません。

第二に、生成と切り替えのコスト — プロセスの生成はメモリ空間を新しく構える仕事なので高く(Linux がこれを安くする装置が次回の copy-on-write です)、スレッドの生成はスタックを一つ載せる仕事なので安いのです。コンテキストスイッチも分かれます。同じプロセスのスレッド間の切り替えはメモリ空間がそのままなので相対的に安く、プロセス間の切り替えはアドレス空間が変わり、キャッシュ・TLB が冷えるコストが上乗せされます。サーバーが遅い理由 #1 で見た過剰なコンテキストスイッチの話が、まさにこのコストです。

第三に、障害の波及範囲 — スレッド一つがメモリを壊して死ぬと、プロセス全体が死にます。 ヒープを共有しているので、一つのスレッドの汚染は全員の汚染だからです。プロセスは一つ死んでも隣は無事です。この性質が、後述する設計選択の最大の根拠になります。

実際のソフトウェアは何を選んだか #

教科書の比較より実物の選択を見るほうが、感覚は速くつかめます。

  • nginx: ワーカープロセスを複数置きます。ワーカーが一つ死んでもサービスは生き、ワーカー同士で共有する状態がほとんどない構造なので、隔離の利点だけを取っています。
  • ウェブブラウザ: タブごとにプロセスを使います。タブが一つ死んでもブラウザが生き残るように、そしてサイト間のメモリ隔離(セキュリティ)のために、高い側をあえて選んだ形です。
  • Java・Go のサーバー: プロセス一つにスレッド(ゴルーチン)多数を置きます。複数のリクエストがコネクションプール・キャッシュのような状態を共有する必要があるので共有メモリの利点が大きく、言語・ランタイムが並行性の道具をよく備えているケースです。
  • Python のウェブサーバー(gunicorn など): プロセス × スレッドの混合です。CPython の GIL のため、一つのプロセスの中のスレッドは CPU の仕事を並列にできないので、CPU の活用はプロセス数で、I/O 待ちの並行性はスレッドで得る折衷です。GIL の詳しい話は モダン Python 上級 #5 で扱いました。

この対比がワーカー設定の読み方になります。「プロセス数は隔離と CPU の並列性、スレッド数は共有状態と I/O の並行性」という軸で読めば、フレームワークの文書の推奨値(例: プロセスはコア数基準、スレッドは I/O の比重基準)がなぜそうなのか見えてきます。

混同しやすい二つ #

  • 「スレッドのほうが常に速い」という誤解 — 生成・切り替えが安いのであって、実行が速いのではありません。CPU を使う計算の速度は同じで、むしろ共有データのロック競合がひどいと、マルチスレッドがマルチプロセスより遅くなります。
  • 並行性 ≠ スレッド — スレッドは並行性を得る複数の手段の一つです。非同期 I/O(イベントループ)はスレッド一つで数万のコネクションを扱い、Python の asyncio・Node.js がこのモデルです。「同時接続が多い = スレッドを増やす」ではなく、仕事が CPU 中心か I/O 待ち中心かが先です。

まとめ #

  • 違いの根はメモリの共有です。プロセスは隔離されたリソースの単位、スレッドはその中でメモリを共有する実行の流れです。
  • 共有は通信を安くする代わりにロックとレースを呼び、隔離は通信を高くする代わりに障害の波及範囲を狭めます。
  • 生成・切り替えのコストはスレッドが安いのですが、実行速度が速くなるわけではありません。
  • 実物の選択(nginx のプロセス、Java のスレッド、Python の混合)は、すべて「共有すべき状態があるか、死んだとき何まで一緒に死んでいいか」への答えです。
  • ワーカー設定は、プロセス数 = 隔離・CPU の並列性、スレッド数 = I/O の並行性という軸で読みます。

次回はプロセスを作る瞬間の話です。fork がどうやって重いプロセス生成を安くするのか、copy-on-write を扱います。

X