メインコンテンツまでスキップ

プロセスとスレッド

所要時間: 30分

前提知識: ps, top コマンドを使ったことがある

学べること:

  • プロセスとは何か
  • スレッドとは何か
  • プロセスとスレッドの違い
  • コンテキストスイッチのコスト
  • Java の Platform Thread と Virtual Thread

この章で答える疑問

「ps で見える PID って何?」
「プロセスとスレッドの違いは?」
「なぜスレッドの方が軽い?」
「Java の Virtual Thread って何がすごいの?」

1. プロセスとは

1.1 プロセスの定義

プロセス = 実行中のプログラム

┌─────────────────────────────────────────────────────────────────────┐
│ │
│ プログラム(静的) → プロセス(動的) │
│ ───────────────── ────────────────── │
│ │
│ ディスク上のファイル → メモリに読み込まれて実行中 │
│ /usr/bin/java → PID: 12345 │
│ │
│ 設計図 → 動いている建物 │
│ │
└─────────────────────────────────────────────────────────────────────┘

1.2 プロセスが持つもの

各プロセスは以下の要素を持っています:

┌─────────────────────────────────────────────────────────────────────┐
│ プロセス (PID: 12345) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 仮想アドレス空間 │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ コード領域(テキストセグメント) │ │ │
│ │ │ → 実行する命令(プログラム本体) │ │ │
│ │ ├──────────────────────────────────────────────────────┤ │ │
│ │ │ データ領域 │ │ │
│ │ │ → グローバル変数、静的変数 │ │ │
│ │ ├──────────────────────────────────────────────────────┤ │ │
│ │ │ ヒープ領域 │ │ │
│ │ │ → 動的に確保されるメモリ(malloc, new) │ │ │
│ │ ├──────────────────────────────────────────────────────┤ │ │
│ │ │ スタック領域 │ │ │
│ │ │ → 関数呼び出し、ローカル変数 │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ プロセス制御ブロック (PCB) │ │
│ │ ├── PID(プロセスID) │ │
│ │ ├── プロセス状態(実行中/待機中/停止中) │ │
│ │ ├── プログラムカウンタ(次の命令のアドレス) │ │
│ │ ├── CPU レジスタの内容 │ │
│ │ ├── メモリ管理情報 │ │
│ │ ├── 開いているファイル一覧(ファイルディスクリプタ) │ │
│ │ ├── 親プロセス ID (PPID) │ │
│ │ └── 優先度、使用時間などの統計情報 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

1.3 Linux での確認

# プロセス一覧
$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 169784 13092 ? Ss Jan01 0:12 /sbin/init
java 5678 25.0 8.5 4568792 697624 ? Sl 10:30 5:23 java -jar app.jar

# 特定プロセスの詳細
$ cat /proc/5678/status
Name: java
State: S (sleeping)
Pid: 5678
PPid: 1234
Threads: 42
VmSize: 4568792 kB # 仮想メモリサイズ
VmRSS: 697624 kB # 物理メモリ使用量

1.4 プロセスの分離

重要: 各プロセスは独立したアドレス空間を持つ

┌───────────────────────────────────────────────────────────────────┐
│ 物理メモリ │
├───────────────────────────────────────────────────────────────────┤
│ │
│ プロセスA プロセスB プロセスC │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ │ │ │ │ │ │
│ │ 仮想 │ │ 仮想 │ │ 仮想 │ │
│ │ アドレス │ │ アドレス │ │ アドレス │ │
│ │ 空間 │ │ 空間 │ │ 空間 │ │
│ │ │ │ │ │ │ │
│ │ ×───────│────X────│───────× │ ← 相互にアクセス不可 │
│ │ │ │ │ │ │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │
└───────────────────────────────────────────────────────────────────┘

プロセスAのメモリ破壊 → プロセスB, Cには影響しない
プロセスAがクラッシュ → プロセスB, Cは動き続ける

この分離により:

  • 安全性: 他のプロセスのメモリを読み書きできない
  • 安定性: 1つのプロセスがクラッシュしても他に影響しない
  • セキュリティ: プロセス間でデータが漏洩しない

2. スレッドとは

2.1 スレッドの定義

スレッド = プロセス内の実行単位

┌─────────────────────────────────────────────────────────────────────┐
│ │
│ 1プロセス = 1スレッド vs 1プロセス = 複数スレッド │
│ ───────────────────── ──────────────────────── │
│ │
│ ┌─────────────────┐ ┌─────────────────────────┐ │
│ │ プロセス │ │ プロセス │ │
│ │ │ │ │ │
│ │ ┌───────────┐ │ │ ┌───┐ ┌───┐ ┌───┐ │ │
│ │ │ スレッド │ │ │ │ T1│ │ T2│ │ T3│ │ │
│ │ └───────────┘ │ │ └───┘ └───┘ └───┘ │ │
│ │ │ │ │ │
│ └─────────────────┘ └─────────────────────────┘ │
│ │
│ シングルスレッド マルチスレッド │
│ │
└─────────────────────────────────────────────────────────────────────┘

2.2 スレッドが共有するもの / 固有のもの

┌─────────────────────────────────────────────────────────────────────┐
│ プロセス (PID: 12345) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 共有リソース │ │
│ │ ├── コード領域 │ │
│ │ ├── データ領域(グローバル変数) │ │
│ │ ├── ヒープ領域 │ │
│ │ ├── ファイルディスクリプタ │ │
│ │ └── シグナルハンドラ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ スレッド1 │ │ スレッド2 │ │ スレッド3 │ │
│ │ (TID: 101) │ │ (TID: 102) │ │ (TID: 103) │ │
│ ├─────────────┤ ├─────────────┤ ├─────────────┤ │
│ │ 固有リソース │ │ 固有リソース │ │ 固有リソース │ │
│ │ ├─ スタック │ │ ├─ スタック │ │ ├─ スタック │ │
│ │ ├─ レジスタ │ │ ├─ レジスタ │ │ ├─ レジスタ │ │
│ │ └─ PC │ │ └─ PC │ │ └─ PC │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

PC = プログラムカウンタ(次に実行する命令のアドレス)

2.3 Linux での確認

# スレッド一覧を表示
$ ps -eLf
UID PID PPID LWP C NLWP STIME TTY TIME CMD
java 5678 1234 5678 0 42 10:30 ? 00:00:05 java -jar app.jar
java 5678 1234 5679 0 42 10:30 ? 00:00:12 java -jar app.jar
java 5678 1234 5680 0 42 10:30 ? 00:00:08 java -jar app.jar
...

# LWP = Light Weight Process (LinuxでのスレッドID)
# NLWP = Number of LWP (スレッド数)

# top でスレッド表示
$ top -H -p 5678
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
5678 java 20 0 4568792 697624 25632 S 0.0 8.5 0:00.05 java
5679 java 20 0 4568792 697624 25632 S 12.0 8.5 0:00.12 GC-Thread
5680 java 20 0 4568792 697624 25632 S 5.0 8.5 0:00.08 http-nio-8080

# /proc でスレッド確認
$ ls /proc/5678/task/
5678 5679 5680 5681 ...

3. プロセス vs スレッド

3.1 比較表

項目プロセススレッド
アドレス空間独立共有
作成コスト高い(数ms〜数十ms)低い(数十μs〜数百μs)
切り替えコスト高い低い
メモリ使用量大きい小さい
通信方法IPC(パイプ、ソケット等)共有メモリ
分離性高い(クラッシュ時の影響が小さい)低い(1スレッドのバグが全体に影響)

3.2 なぜスレッドは軽いのか?

┌─────────────────────────────────────────────────────────────────────┐
│ プロセス作成の場合 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ fork() システムコール │
│ │ │
│ ├── 新しいアドレス空間を作成 │
│ ├── ページテーブルをコピー(または CoW 設定) │
│ ├── 新しい PCB を作成 │
│ ├── ファイルディスクリプタテーブルをコピー │
│ ├── シグナルハンドラをコピー │
│ └── カーネルのデータ構造を更新 │
│ │
│ → 多くのリソースを複製する必要がある │
│ │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│ スレッド作成の場合 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ pthread_create() / clone() │
│ │ │
│ ├── 新しいスタック領域を確保 │
│ ├── 新しいスレッド制御ブロックを作成 │
│ └── レジスタ、PC を初期化 │
│ │
│ → アドレス空間、FDなどは共有するので複製不要 │
│ │
└─────────────────────────────────────────────────────────────────────┘

3.3 使い分け

┌─────────────────────────────────────────────────────────────────────┐
│ 使い分けの指針 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ プロセスを選ぶ場合: │
│ ├── 分離が必要(1つの障害が他に影響しないようにしたい) │
│ ├── 異なるプログラムを実行(例: シェルからコマンド実行) │
│ ├── セキュリティ境界を設けたい │
│ └── 例: Web サーバーのワーカープロセス(Apache prefork) │
│ │
│ スレッドを選ぶ場合: │
│ ├── 高頻度で作成・破棄 │
│ ├── データを共有したい │
│ ├── 応答性が重要 │
│ └── 例: HTTPリクエスト処理、バックグラウンドタスク │
│ │
└─────────────────────────────────────────────────────────────────────┘

4. コンテキストスイッチ

4.1 コンテキストスイッチとは

CPU が別の実行単位に切り替わる際の処理。

┌─────────────────────────────────────────────────────────────────────┐
│ コンテキストスイッチの流れ │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ スレッドA実行中 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 1. 割り込み発生 │ │
│ │ (タイマー、I/O完了、システムコール) │
│ └─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 2. スレッドAの状態を保存 │ │
│ │ ├── CPU レジスタ │ │
│ │ ├── プログラムカウンタ │ │
│ │ └── スタックポインタ │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 3. スケジューラが次のスレッドを選択 │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 4. スレッドBの状態を復元 │ │
│ │ ├── CPU レジスタ │ │
│ │ ├── プログラムカウンタ │ │
│ │ └── スタックポインタ │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ スレッドB実行開始 │
│ │
└─────────────────────────────────────────────────────────────────────┘

4.2 コンテキストスイッチのコスト

┌─────────────────────────────────────────────────────────────────────┐
│ 直接的コスト │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ レジスタの保存・復元 : 数十〜数百ナノ秒 │
│ TLB (Translation Lookaside Buffer) フラッシュ │
│ → プロセス切り替え時のみ : 数マイクロ秒 │
│ │
├─────────────────────────────────────────────────────────────────────┤
│ 間接的コスト(より大きい) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ CPU キャッシュのミス : 数十〜数百マイクロ秒の遅延 │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ スレッドA実行中 │ │
│ │ ├── L1/L2/L3 キャッシュにデータがある(ホット) │ │
│ │ │ │
│ │ スレッドBに切り替え │ │
│ │ ├── スレッドBのデータはキャッシュにない │ │
│ │ ├── メモリから読み込む必要がある │ │
│ │ └── 「キャッシュミス」が多発 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

※ 頻繁なコンテキストスイッチは性能低下の原因

4.3 コンテキストスイッチの確認

# vmstat でコンテキストスイッチを確認
$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 8234560 234512 4567832 0 0 0 5 256 1234 15 5 80 0 0
1 0 0 8234560 234512 4567832 0 0 0 0 312 2345 20 8 72 0 0

# cs = context switches per second

# 特定プロセスのコンテキストスイッチ
$ cat /proc/5678/status | grep ctxt
voluntary_ctxt_switches: 12345 # 自発的(I/O待ちなど)
nonvoluntary_ctxt_switches: 678 # 非自発的(タイムスライス切れ)

5. Java のスレッドモデル

5.1 Platform Thread(従来のスレッド)

┌─────────────────────────────────────────────────────────────────────┐
│ Platform Thread │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 特徴: │
│ ├── 1つの Java Thread = 1つの OS Thread │
│ ├── OS がスケジューリング │
│ ├── スタックサイズ: デフォルト 1MB(-Xss で設定) │
│ └── 作成コスト: 高い(システムコールが必要) │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ JVM │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Thread │ │ Thread │ │ Thread │ │ │
│ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │
│ └───────│────────────│────────────│────────────────────────┘ │
│ │ │ │ 1:1 マッピング │
│ ┌───────▼────────────▼────────────▼────────────────────────┐ │
│ │ OS │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │OS Thread│ │OS Thread│ │OS Thread│ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ 制限: │
│ ├── 1スレッド 1MB × 10,000スレッド = 10GB のスタックメモリ │
│ ├── OS スレッド数に上限がある │
│ └── コンテキストスイッチのオーバーヘッド │
│ │
└─────────────────────────────────────────────────────────────────────┘

5.2 Virtual Thread(Java 21+)

┌─────────────────────────────────────────────────────────────────────┐
│ Virtual Thread (Project Loom) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 特徴: │
│ ├── 軽量スレッド(JVM が管理) │
│ ├── スタックサイズ: 数KB〜(必要に応じて成長) │
│ ├── 作成コスト: 非常に低い │
│ └── 数百万スレッドも可能 │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ JVM │ │
│ │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ │
│ │ │VT1│ │VT2│ │VT3│ │VT4│ │VT5│ │VT6│ │VT7│ │VT8│ ... │ │
│ │ └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ └─────┼─────┴─────┼─────┴─────┼─────┴─────┘ │ │
│ │ │ │ │ │ │
│ │ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │ │
│ │ │Carrier │ │Carrier │ │Carrier │ ← Platform Thread│ │
│ │ │Thread │ │Thread │ │Thread │ │ │
│ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │
│ └──────────│───────────│───────────│────────────────────────┘ │
│ │ │ │ │
│ ┌──────────▼───────────▼───────────▼────────────────────────┐ │
│ │ OS │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │OS Thread│ │OS Thread│ │OS Thread│ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ M:N マッピング(多数の Virtual Thread : 少数の OS Thread) │
│ │
└─────────────────────────────────────────────────────────────────────┘

5.3 Virtual Thread の動作

┌─────────────────────────────────────────────────────────────────────┐
│ Virtual Thread のライフサイクル │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Virtual Thread が I/O でブロック: │
│ │
│ 1. VT1 が DB クエリを実行 │
│ │ │
│ ▼ │
│ 2. I/O 待ちが発生 │
│ │ │
│ ▼ │
│ 3. VT1 が Carrier Thread から「アンマウント」される │
│ ├── VT1 の状態を保存(ヒープに) │
│ └── Carrier Thread は空く │
│ │ │
│ ▼ │
│ 4. 別の Virtual Thread (VT5) が Carrier Thread に「マウント」 │
│ └── VT5 が実行を開始 │
│ │ │
│ ▼ │
│ 5. VT1 の I/O が完了 │
│ │ │
│ ▼ │
│ 6. VT1 が別の Carrier Thread にマウントされて再開 │
│ │
└─────────────────────────────────────────────────────────────────────┘

従来: I/O 待ち中も OS Thread を占有 → スレッド数に制限
VT: I/O 待ち中は OS Thread を解放 → 大量の並行処理が可能

5.4 比較: 10,000 同時リクエスト処理

┌─────────────────────────────────────────────────────────────────────┐
│ Platform Thread の場合 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 10,000 リクエスト = 10,000 OS Thread が必要 │
│ │
│ リソース使用量: │
│ ├── メモリ: 1MB × 10,000 = 10GB (スタックのみ) │
│ ├── OS リソース: スレッド管理のオーバーヘッド │
│ └── コンテキストスイッチ: 頻繁に発生 │
│ │
│ → 実際には数千スレッドが現実的な上限 │
│ │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│ Virtual Thread の場合 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 10,000 リクエスト = 10,000 Virtual Thread │
│ 実際の OS Thread = CPU コア数程度(例: 16) │
│ │
│ リソース使用量: │
│ ├── メモリ: 数KB × 10,000 = 数十MB (スタック) │
│ ├── OS リソース: 16 OS Thread のみ │
│ └── コンテキストスイッチ: 少ない │
│ │
│ → 数十万〜数百万の並行処理が可能 │
│ │
└─────────────────────────────────────────────────────────────────────┘

5.5 Virtual Thread のコード例

// Platform Thread(従来)
ExecutorService executor = Executors.newFixedThreadPool(200);
executor.submit(() -> {
// 最大200並行
});

// Virtual Thread(Java 21+)
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
executor.submit(() -> {
// 事実上無制限の並行
});

// さらにシンプルに
Thread.startVirtualThread(() -> {
// Virtual Thread で実行
});

6. まとめ

┌─────────────────────────────────────────────────────────────────────┐
│ この章で学んだこと │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. プロセス │
│ ├── 実行中のプログラム │
│ ├── 独立したアドレス空間を持つ │
│ └── ps, /proc で確認できる │
│ │
│ 2. スレッド │
│ ├── プロセス内の実行単位 │
│ ├── アドレス空間を共有、スタックは固有 │
│ └── ps -eLf, top -H で確認できる │
│ │
│ 3. プロセス vs スレッド │
│ ├── プロセス: 分離性高い、コスト高い │
│ └── スレッド: 分離性低い、コスト低い │
│ │
│ 4. コンテキストスイッチ │
│ ├── 切り替え時にレジスタ保存・復元 │
│ └── キャッシュミスが性能低下の原因 │
│ │
│ 5. Virtual Thread (Java 21+) │
│ ├── M:N マッピング(多数VT : 少数OS Thread) │
│ ├── I/O 待ち中は OS Thread を解放 │
│ └── 大量の並行処理が可能 │
│ │
└─────────────────────────────────────────────────────────────────────┘

確認問題

  1. プロセスとスレッドの違いを説明してください
  2. なぜスレッドの方が作成コストが低いのですか?
  3. コンテキストスイッチで何が起きていますか?
  4. Virtual Thread が従来のスレッドより効率的な理由は?
  5. ps auxps -eLf の違いは?

次のステップ