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が状態を動かす
ディスパッチ
タイムスライス切れ
I/Oを要求(待機へ)
I/O完了(実行可能へ)
⏸ 一時停止
3つの円=状態。矢印=許された遷移。ボタンは今いる状態から出られる遷移のときだけ働きます(放っておくと自動でもっともらしく動きます)。実行中→待機 はディスクなど遅いI/Oを待つとき、完了すると待機→実行可能 で列に戻ります。
POINT — 「待機」があるからCPUが遊ばない
ディスクやネットワークはCPUより桁違いに遅い。もし待っているプロセスをCPUに載せたままにしたら、その間CPUは何もできず丸損だ。だからI/O待ちのプロセスは待機 へ外し、その隙に別の実行可能なプロセスを走らせる。この「待つ者は退く」仕組みが、CPUを遊ばせない鍵になっている。
4. スレッドが軽いわけ — 切り替えコスト
同じプロセス内のスレッド間 の切り替えは、レジスタとスタックの入れ替えだけで済みます。ところが別プロセス間 の切り替えでは、それに加えてメモリ空間(ページテーブル)の切り替え とTLBのフラッシュ が要り、キャッシュも冷えます。だから「スレッドは軽い」のです。
2つのボタンで切り替えの中身を比べてください。同じプロセス内かどうかで、退避・復元すべき項目の数がまるで違います。
切り替えコスト — スレッド間 vs プロセス間
スレッド間の切り替え
プロセス間の切り替え
⏸ 一時停止
各行=切り替えで発生する作業。今のモードに含まれる項目が順に✓で点灯し、右の点の数=相対コスト。下のバー=合計コスト。スレッド間は上2つだけで「軽い」、プロセス間はページテーブル切替・TLBフラッシュが加わって「重い」ことが一目で分かります。
スレッド間の切替コスト ≪ プロセス間の切替コスト
スレッドは同じアドレス空間を共有するので、ページテーブルの切り替えもTLBのフラッシュも不要。これが「軽量スレッド」と呼ばれる理由。
5. まとめ — 実行を組み立てる部品
プロセス :独立したメモリ空間を持つ実行の入れ物。コード・ヒープ・スタックを含む。
スレッド :プロセス内の実行の流れ。コードとヒープを共有し、スタックとレジスタは固有。
コンテキストスイッチ :レジスタを退避・復元して実行を切り替える。プロセス間はさらにアドレス空間の切替が要り重い。
状態遷移 :実行中・実行可能・待機を行き来。I/O待ちは待機へ外し、CPUを遊ばせない。
注意 — 「並行」と「並列」は違う
1個のCPUでスレッドを高速に切り替えて「同時っぽく」見せるのが並行(concurrency) 。複数のコアで本当に同時に走らせるのが並列(parallelism) 。マルチスレッドで書いても、コアが1つなら実行は同時ではない。この違いを取り違えると「スレッドを増やせば必ず速くなる」という誤解に陥る。
一歩先へ — カーネルスレッドとユーザースレッド、そして先へ
スレッドをOSが管理するか(カーネルスレッド)、アプリ側で軽く回すか(ユーザースレッド/コルーチン)で設計が分かれる。Go の goroutine や各言語の async/await は、OSスレッドより桁違いに軽い「見かけの実行単位」を大量に走らせる工夫だ。誰がどう切り替えるかを制御することが、次のスケジューリング 、そして並行制御と同期 へとつながっていく。