プロセスとスレッド — 実行という「動き」の正体

プログラムはただのファイル。それをメモリに読み込んで動き出したものがプロセスです。プロセスは自分だけのメモリ空間を持ち、その中で1本以上のスレッドが命令を実行します。CPUがどうやって複数の実行を切り替え(コンテキストスイッチ)、プロセスが実行中・実行可能・待機のあいだをどう行き来するのか — 「動いている」の中身を分解します。

1. メモリの持ち方 — プロセスとスレッド

プロセスは独立したメモリ空間を1つ持ちます。その中にはコード(命令)、データ・ヒープ(動的に確保する領域)、そしてスタックがあります。1つのプロセスの中に複数のスレッドを作ると、スレッドたちはコードとヒープを共有しますが、スタックとレジスタはそれぞれ固有に持ちます。だから同じ変数を全スレッドから触れる一方、関数呼び出しの途中経過は互いに干渉しません。

スレッド数を変えて、共有される領域とスレッドごとに増える領域を見比べてください。

1つのプロセス、複数のスレッド — 何を共有し何を分けるか
紫の外枠=プロセスのメモリ空間。青い「コード」と緑の「ヒープ」は全スレッド共有(中央の共有変数 counter に各スレッドから線が伸びます)。下の色つき縦棒=スレッドごとの固有のスタック(伸び縮みが独立)。スレッドを増やすと、共有領域はそのまま、スタックだけが増えます。
POINT — 共有するから速く、共有するから危ない スレッドはメモリ空間を共有するので、データのやり取りにコピーがいらず軽くて速い。プロセスを分けると通信にわざわざIPC(プロセス間通信)が必要になる。ただし同じヒープを複数スレッドが同時に書き換えるとデータ競合が起きる。この危うさの制御が、後の「並行制御と同期」のテーマになる。

2. コンテキストスイッチ — レジスタの退避と復元

CPUのレジスタ(PC=次に実行する命令の位置、SP=スタックの先端、汎用レジスタ…)は1組しかありません。だから別のスレッドに実行を移すときは、いま動いているスレッドのレジスタを丸ごと保存領域(TCB)に退避し、次のスレッドのレジスタを復元してから走り出します。この入れ替え作業がコンテキストスイッチです。

「コンテキストスイッチ」ボタンで、レジスタの値がCPUと保存領域のあいだを行き来する様子を見てください。

コンテキストスイッチ — レジスタファイルの入れ替え
中央=CPUのレジスタファイル(実行中スレッドの値が入る)。左右=各スレッドの保存領域(TCB)。切り替え時、まず黄の点=実行中スレッドのレジスタを退避し、次に緑の点=次スレッドのレジスタを復元。実行中スレッドのTCBは「値はCPU上」と薄く表示されます。
切り替え時間 = レジスタの退避(保存)+ レジスタの復元(読み込み)+ α 1回あたり数百ナノ秒〜数マイクロ秒。切り替えが多すぎると、この「オーバーヘッド」だけでCPU時間を食い、本来の仕事が進まなくなる。

3. プロセスの一生 — 実行中・実行可能・待機

いつでも動けるわけではありません。プロセス(やスレッド)は3つの状態を行き来します。CPUを使っている実行中(Running)、順番待ちの実行可能(Ready)、そしてI/O完了などを待つ待機(Blocked)。スケジューラが実行可能なものを選んで走らせ(ディスパッチ)、タイムスライスが切れれば実行可能へ戻し、I/Oを要求すれば待機へ送ります。

ボタンでイベントを起こし、プロセス(ピンクの点)が状態のあいだを移る様子を追ってください。特にI/Oを要求 → 待機、I/O完了 → 実行可能の流れが要です。

状態遷移 — スケジューラとI/Oが状態を動かす
3つの円=状態。矢印=許された遷移。ボタンは今いる状態から出られる遷移のときだけ働きます(放っておくと自動でもっともらしく動きます)。実行中→待機はディスクなど遅いI/Oを待つとき、完了すると待機→実行可能で列に戻ります。
POINT — 「待機」があるからCPUが遊ばない ディスクやネットワークはCPUより桁違いに遅い。もし待っているプロセスをCPUに載せたままにしたら、その間CPUは何もできず丸損だ。だからI/O待ちのプロセスは待機へ外し、その隙に別の実行可能なプロセスを走らせる。この「待つ者は退く」仕組みが、CPUを遊ばせない鍵になっている。

4. スレッドが軽いわけ — 切り替えコスト

同じプロセス内のスレッド間の切り替えは、レジスタとスタックの入れ替えだけで済みます。ところが別プロセス間の切り替えでは、それに加えてメモリ空間(ページテーブル)の切り替えとTLBのフラッシュが要り、キャッシュも冷えます。だから「スレッドは軽い」のです。

2つのボタンで切り替えの中身を比べてください。同じプロセス内かどうかで、退避・復元すべき項目の数がまるで違います。

切り替えコスト — スレッド間 vs プロセス間
各行=切り替えで発生する作業。今のモードに含まれる項目が順に✓で点灯し、右の点の数=相対コスト。下のバー=合計コスト。スレッド間は上2つだけで「軽い」、プロセス間はページテーブル切替・TLBフラッシュが加わって「重い」ことが一目で分かります。
スレッド間の切替コスト ≪ プロセス間の切替コスト スレッドは同じアドレス空間を共有するので、ページテーブルの切り替えもTLBのフラッシュも不要。これが「軽量スレッド」と呼ばれる理由。

5. まとめ — 実行を組み立てる部品

注意 — 「並行」と「並列」は違う 1個のCPUでスレッドを高速に切り替えて「同時っぽく」見せるのが並行(concurrency)。複数のコアで本当に同時に走らせるのが並列(parallelism)。マルチスレッドで書いても、コアが1つなら実行は同時ではない。この違いを取り違えると「スレッドを増やせば必ず速くなる」という誤解に陥る。
一歩先へ — カーネルスレッドとユーザースレッド、そして先へ スレッドをOSが管理するか(カーネルスレッド)、アプリ側で軽く回すか(ユーザースレッド/コルーチン)で設計が分かれる。Go の goroutine や各言語の async/await は、OSスレッドより桁違いに軽い「見かけの実行単位」を大量に走らせる工夫だ。誰がどう切り替えるかを制御することが、次のスケジューリング、そして並行制御と同期へとつながっていく。