プロセスとスレッドの違いは、OSが「何を分離するか」と「何をCPUで再開するか」です。プロセスは、プログラムを動かすための仮想アドレス空間や権限、開いた資源をまとめた資源・保護の境界です。スレッドは、その境界の内側でプログラムカウンタ、レジスタ、スタックを持ち、OSがCPUへ割り当てる実行文脈です。

「プロセスは作業場所、スレッドは作業担当」と覚えるだけでは、なぜデータ共有が速いのか、なぜ競合するのか、CPUが何を切り替えるのかまでは分かりません。この記事では、その比喩をメモリとCPUの実体へ戻します。
- 起動前のプログラムは、まだプロセスではない
- プロセスは「資源」と「保護」の境界
- スレッドは、CPUが途中から再開できる「実行文脈」
- memo.txtの入力・画面・保存を3本のスレッドで追う
- 同じプロセス内の共有は速い。しかし、順序は自動ではそろわない
- 別プロセスなら、なぜIPCが必要なのか
- 1コアでも複数スレッドが進むのは、OSが実行文脈を切り替えるから
- スレッドを増やしても、必ず速くなるわけではない
- Windows・Linux・POSIXで共通する芯と異なる実装
- 実機ではPID・TID・メモリマップ・待機を対応付ける
- JavaのThreadとRunnableへつなげる
- まとめ:プロセスは境界、スレッドは再開できる状態
起動前のプログラムは、まだプロセスではない
ストレージ上のテキストエディタの実行ファイルは、命令や初期データを一定の形式で並べたプログラムファイルです。ファイルが置かれているだけでは、次に実行する命令位置も、作業中の文字列も、CPUへ割り当てる実行文脈もありません。
エディタを起動すると、OSとプログラムローダーが実行環境を準備します。細部はOSや実行形式によって異なりますが、概念上は次のものがそろった時点で「実行中」になります。
- プロセスを識別するIDやセキュリティ情報を用意する
- そのプロセス用の仮想アドレス空間を作る
- 実行コード、初期データ、必要な共有ライブラリなどをアドレス空間へ対応付ける
- 動的なデータに使うヒープや、最初のスレッドに使うスタックを用意する
- 最初のスレッドの開始位置とレジスタ状態を作り、実行可能な状態にする
WindowsのCreateProcessは、新しいプロセスとその主スレッドを作ります。一方、Linuxを含むPOSIX系では、既存プロセスを複製してからexecveでプログラムイメージを置き換える形や、posix_spawnを使う形があります。つまり「ファイルを起動するとプロセスができる」という結果は同じでも、生成手順はOS APIごとに同一ではありません。
プロセスは「資源」と「保護」の境界
「プロセス=実行中のプログラム」は入口として正しい説明です。ただし本質をつかむには、実行に必要な資源をまとめ、別のプロセスから原則として分離する境界だと捉える必要があります。
たとえばエディタのプロセスには、次のような要素があります。
| 要素 | 役割 | 例 |
|---|---|---|
| 仮想アドレス空間 | コードやデータへ仮想アドレスでアクセスする範囲 | 実行コード、静的データ、ヒープ、共有ライブラリ、各スレッドのスタック |
| OS資源への参照 | カーネルが管理する対象を利用するための識別子 | 開いたmemo.txtのハンドルやファイルディスクリプタ |
| 実行条件 | どの権限・設定で動くかを決める情報 | ユーザー資格情報、環境変数、カレントディレクトリ |
| 識別・管理情報 | OSが実行環境を追跡するための情報 | PID、資源使用量、終了状態 |
プロセスAの仮想アドレス0x1000と、プロセスBの0x1000は、同じ物理メモリを意味するとは限りません。OSとCPUのアドレス変換機構がプロセスごとの対応表を使うため、Aの誤ったポインタがBのヒープを通常のメモリアクセスで自由に書き換えないようにできます。これが保護境界です。
ただし完全に何も共有しないわけではありません。読み取り専用のライブラリコードを物理的に共有したり、明示的な共有メモリを複数プロセスへ割り当てたりできます。重要なのは、別プロセス間の共有はOSへ明示し、権限と対応付けを作る必要があることです。
スレッドは、CPUが途中から再開できる「実行文脈」
スレッドを単なる「処理の流れ」と呼ぶと、流れの正体が見えません。CPUが処理を中断し、あとで同じ続きから再開するには、少なくとも次に実行する位置と計算途中の状態を覚えておく必要があります。そのまとまりがスレッドの実行文脈です。
| 各スレッドが個別に持つもの | なぜ個別なのか |
|---|---|
| プログラムカウンタ(PC/命令ポインタ) | スレッドごとに「次にどの命令から続けるか」が違うため |
| 汎用レジスタなどのCPU状態 | 計算途中の値やアドレスがスレッドごとに違うため |
| スタックポインタとスタック領域 | 呼び出し中の関数、戻り先、局所変数がスレッドごとに違うため |
| スケジューリング状態 | 実行中、実行可能、待機中などの状態がスレッドごとに違うため |
一方、同じプロセスのスレッドは、コード、静的データ、ヒープ、読み込んだライブラリ、開いたファイルなど、プロセス側の資源を共有します。スタックは各スレッド専用ですが、そのメモリ領域自体はプロセスの仮想アドレス空間の中にあります。


ここで「作業場所と作業担当」の比喩へ戻すと、作業場所に相当するのは共有ヒープや開いた資源です。作業担当に相当するのは、PC・レジスタ・スタックを持つ実行文脈です。この対応まで言えれば、比喩が実体につながります。
memo.txtの入力・画面・保存を3本のスレッドで追う
説明用に、テキストエディタが次の3本を使うとします。実際のスレッド構成や画面更新の制約は、エディタやGUIフレームワークによって異なります。
- UIスレッド:キー入力を受け取り、編集内容を更新して画面を描く
- 保存スレッド:保存操作を受け、
memo.txtへ書き込む - 自動保存スレッド:一定条件で編集内容のスナップショットを保存する
1.UIスレッドがAを編集バッファへ加える
キーボード入力の通知を受けたUIスレッドが実行可能になると、OSのスケジューラは空いた論理CPUへそのスレッドを割り当てます。CPUにはUIスレッドのPC、レジスタ、スタック位置が復元され、前回止まった命令の続きから動きます。
UIスレッドは、ヒープにあるmemo.txtの編集バッファへAを追加します。このヒープはUIスレッド専用ではなく、同じプロセスの保存スレッドや自動保存スレッドからも同じ仮想アドレスで参照できます。CPUが命令を実行する基本はCPUとはで詳しく扱っています。
2.保存スレッドは同じバッファからデータを受け取れる
保存ボタンが押されると、UIスレッドが保存要求をキューへ入れ、保存スレッドを起こす設計が考えられます。保存スレッドは別プロセスへ文字列を送り直す必要がなく、同じヒープ上の編集データを参照できます。必要なのは「どの時点の内容を保存するか」を同期することです。
保存スレッドがOSへ書き込みを依頼すると、ストレージI/Oの完了待ちになる場合があります。このとき待機しているのは保存スレッドであり、CPUそのものではありません。OSは保存スレッドを待機状態へ移し、UIスレッドや別アプリの実行可能なスレッドへCPUを渡せます。アプリからOSへ依頼する境界はシステムコールとはでつながります。
3.I/O完了後、保存スレッドは止めた続きへ戻る
I/Oの完了が通知されると、保存スレッドは待機中から実行可能へ戻ります。ただし、その瞬間に必ずCPUを得るとは限りません。スケジューラが優先度や他の実行可能スレッド、利用できるCPUなどを見て選び、選ばれたときに保存スレッドのPCとレジスタを復元します。
復元されたPCは「書き込み依頼の次に処理すべき命令」を示します。だから保存スレッドは、I/O待ちの間に別のスレッドが何度動いても、自分の続きから完了処理を再開できます。
同じプロセス内の共有は速い。しかし、順序は自動ではそろわない
同じプロセスのスレッド間では、ヒープ上のオブジェクトを同じアドレスで参照できます。別プロセス向けのメッセージ形式へ変換し、カーネルのIPC経路で渡し、相手側で復元する手順を省けるため、プロセス間通信と比べて小さなデータを頻繁に共有しやすいのが利点です。
ただし「同じメモリが見える」と「複数の操作がひとまとまりで正しい順序に見える」は別です。次の自動保存を考えます。
| 順序 | UIスレッド | 自動保存スレッド |
|---|---|---|
| 1 | Aを追加し、未保存フラグをON | ― |
| 2 | ― | Aまでの内容を保存用に読む |
| 3 | Bを追加し、未保存フラグをON | ― |
| 4 | ― | Aの保存を終え、未保存フラグをOFF |
最後のメモリ上にはABがありますが、ストレージにはAまでしかなく、未保存フラグはOFFです。正しさがスレッドの実行順序に依存して壊れるのがrace condition(競合状態)の一例です。単一コアでも、手順2と3の間でスレッドが切り替われば起こり得ます。
対策は、共有する「編集内容・版番号・保存済み状態」の関係を設計し、mutexなどのロック、atomic操作、メッセージキュー、変更不能なスナップショットなどで同期することです。たとえば短時間だけロックして内容と版番号のスナップショットを作り、I/O完了後は「保存した版番号」を更新すれば、現在の版との差から未保存か判断できます。
別プロセスなら、なぜIPCが必要なのか
自動保存を別プロセスへ分けた場合、そのプロセスはエディタとは別の仮想アドレス空間を持ちます。エディタのヒープを指すポインタ値だけ渡しても、自動保存プロセスでは同じデータを指しません。通常のメモリアクセスも保護機構に拒否されます。
そこで、pipe、socket、メッセージ、RPC、共有メモリ、ファイルマッピングなどのIPC(Inter-Process Communication/プロセス間通信)を使います。OSは、通信相手、権限、データの受け渡し、共有ページの対応付けなどを仲介します。
| 方式 | データの渡し方 | 主な注意点 |
|---|---|---|
| pipe/socket/メッセージ | 送信側のデータを通信単位へし、受信側へ渡す | コピー、形式変換、境界の設計が必要 |
| 共有メモリ/ファイルマッピング | 同じ物理ページ等を双方のアドレス空間へ明示的に対応付ける | 速く共有しやすいが、ロック等の同期は別途必要 |
| ファイル | 双方が同じ名前付きデータを読み書きする | 更新の原子性、整合性、永続化の扱いが必要 |
この手間と引き換えに、一方のプロセスが不正なアドレスへ書いて異常終了しても、別プロセスの私有メモリまで直接壊しにくくなります。権限もプロセス単位などで分けられます。そのためブラウザのように、安定性や権限分離を優先して1つのアプリを複数プロセスへ分ける設計もあります。
1コアでも複数スレッドが進むのは、OSが実行文脈を切り替えるから
ある論理CPUが、ある瞬間に実行するのは、OSに選ばれた1本のスレッドの実行文脈です。実行可能なスレッドが複数あれば、OSは一定時間の経過、優先度の高いスレッドの準備完了、実行中スレッドの待機などを契機に、別のスレッドへ切り替えます。
概念上のcontext switch(コンテキストスイッチ)では、現在のスレッドのPC・レジスタ・スタック位置など、再開に必要な状態を保存し、次のスレッドの状態をCPUへ復元します。別プロセスのスレッドへ移る場合は、利用する仮想アドレス空間の対応も切り替わります。実際にどの状態をいつ保存するか、キャッシュやアドレス変換情報へどの程度影響するかはCPUとOSの実装で異なります。
| 時点 | 1つの論理CPUで動くもの | 保存スレッドの状態 |
|---|---|---|
| ① Aを入力 | UIスレッド | 実行可能または待機 |
| ② 保存要求を処理 | 保存スレッド | 実行中 |
| ③ ストレージ完了待ち | UIスレッドや別アプリのスレッド | 待機中 |
| ④ I/O完了後 | スケジューラに選ばれたスレッド | 実行可能から実行中へ戻り得る |
このように、複数処理がある時間幅の中で少しずつ前進する状態が並行(concurrent)です。複数の論理CPUがUIスレッドと保存スレッドを同じ瞬間に実行する状態が並列(parallel)です。並列なら並行でもありますが、1コア上の時分割による並行は並列ではありません。
スレッドを増やしても、必ず速くなるわけではない
スレッドを分ける価値は、処理時間の短縮だけではありません。保存スレッドがI/Oを待つ間もUIスレッドを動かせれば、保存自体が速くならなくても画面の応答性は保ちやすくなります。まず「完了を速めたいのか」「待ちの間も別の仕事を進めたいのか」を分けます。
| 増やしても速くならない主因 | 内部で起きること |
|---|---|
| 逐次処理が残る | 前の結果が必要な部分は同時に進められない |
| CPU数を超えるCPU処理 | 実行可能スレッドが待ち、context switchも増える |
| 同じロックへ集中する | 同時に走れても共有状態の前で直列化される |
| メモリ帯域・キャッシュを競合する | コアが空いていてもデータ供給が追い付かない |
| 同じI/O機器を使う | 要求キューや機器がボトルネックなら、待ちを増やすだけになる |
| 細かすぎる仕事へ分割する | 作成、スケジューリング、同期の費用が本体処理を上回る |
したがって「スレッドはプロセスより軽いから大量に作る」が結論ではありません。同一プロセスのスレッドはアドレス空間を共有するため、別プロセスより作成・切替の負担が小さいことが多い一方、実際のコストはOS、CPU、ランタイム、スレッドの状態で変わります。一般論だけで本数を決めず、CPU使用率、実行可能な待ち、I/O待ち、ロック競合、context switch数を観測します。
Windows・Linux・POSIXで共通する芯と異なる実装
この記事の「プロセスは資源・保護境界、スレッドは実行文脈」というモデルは、現代のWindowsやLinuxで使える共通の理解です。実際に、Microsoftのprocess/thread資料はaddress spaceとthread contextを分け、Linuxのpthreads資料はdata・heapの共有とthreadごとのstackを分けています。ただし、用語の属性や実装細部まで全OSで同じではありません。
| 範囲 | 共通して使える理解 | 異なり得る点 |
|---|---|---|
| Windows | プロセスが仮想アドレス空間と資源を持ち、スレッドがスケジュールされる | プロセス生成API、スレッド状態名、優先度・quantum、保存するcontext構造 |
| Linux | 同一プロセスのPOSIX threadはdata・heap等を共有し、各threadはstack等を持つ | clone系の共有フラグ、NPTLの実装、scheduler policy、niceやIDの扱い |
| POSIX | thread API、mutex、共有・個別属性などの移植可能な契約を定める | カーネル内部のデータ構造、CPU割当アルゴリズム、アドレス空間の具体的配置は規定しない |
さらに、言語ランタイムは「タスク」「コルーチン」「仮想スレッド」など、OSスレッドより上位の実行単位を持つことがあります。ここで扱ったのは主にOSがスケジュールするネイティブスレッドのモデルです。アプリから見える実行単位がOSスレッドと1対1かどうかは、利用するランタイムの仕様を確認します。
実機ではPID・TID・メモリマップ・待機を対応付ける
Windowsのタスクマネージャーでアプリ別に見えるのは主にプロセスです。「詳細」等でPIDを確認し、必要に応じてスレッド数やCPU使用率を表示すると、1つのプロセス内に複数の実行文脈があることを観測できます。詳しい性能分析ではWindows Performance Analyzerなどでthread stateやcontext switchを追います。
Linuxでは、権限の範囲内で次の情報を対応付けられます。
# PID 1234に属するthreadと現在の状態・実行CPUを見る例
ps -L -p 1234 -o pid,tid,stat,psr,pcpu,comm
# process内のthread IDを列挙
ls /proc/1234/task/
# processの仮想メモリ領域と権限を見る
cat /proc/1234/maps
mapsには実行ファイル、共有ライブラリ、heap、stackなどの対応が見えます。ただし、表示名や可視範囲はカーネル版・権限・デバッグ制限で異なり、個々の行がソースコードの変数へそのまま対応するわけではありません。
JavaのThreadとRunnableへつなげる
JavaのThreadを理解するときも、「何をCPUで再開できる実行文脈として扱うか」が土台です。start()は同じメソッドを普通に呼ぶのではなく、別の実行の流れを開始する操作だと捉えられます。
Runnableは実行したい処理を表し、Threadはその処理を進める実行の流れに関係します。ただし、Java APIとOSスレッドの対応はJVMやThreadの種類によって異なるため、常に1対1だと決め付けないことが大切です。JVM内のheapやstackはJavaのメモリ管理で区別できます。
まとめ:プロセスは境界、スレッドは再開できる状態
memo.txtへAを入力して保存するとき、プロセスは編集バッファや開いたファイルを共有できる境界を用意します。その中でUIスレッドと保存スレッドは、それぞれのPC・レジスタ・スタックを持ち、OSに選ばれたときだけCPU上で続きを実行します。保存スレッドがI/Oを待てばCPUは別スレッドへ移り、同じヒープを同時に更新するなら同期が必要になります。「資源を分けるプロセス」と「実行状態を切り替えるスレッド」という因果までつなげると、2つの違いを暗記ではなく説明できます。
