中級では「分離=安全だが重い/共有=速いが危うい」という対を見ました。上級では、その 重さの正体(切り替えコスト)と 危うさの正体(可視性と同期)を、CPUとメモリの動きまで下りて追います。
上級の解説は準備中のため、上級の内容を表示しています。
概要 — まず全体をつかむ
詳細 — 1段階ずつ追う
プロセスのメモリレイアウト
プロセスが持つ「自分専用のメモリ」は、内部で役割ごとに区画が分かれています。
- テキスト領域 — 機械語の命令そのもの。書き換え禁止で読むだけ
- データ領域 — 初期値を持つグローバル変数など(プログラム開始時に確定)
- ヒープ ※1 — 実行中に必要な分だけ確保する領域。低い番地から上へ伸びる
- スタック ※2 — 関数呼び出しの記録(戻り先・局所変数)。高い番地から下へ伸びる
ヒープとスタックは互いに向き合って伸び、メモリ の空間を挟み込むように使います。スレッドはこのうち テキスト・データ・ヒープを共有 し、スタックだけを各自持つ ——ここが「速いが危うい」の出発点です。共有部分は受け渡しなしで触れて速い反面、同じ番地を複数のスレッドが同時にいじれてしまうのです。
登場人物メモ:
- ※1 ヒープ — 実行中に動的に確保・解放するメモリ領域
- ※2 スタック — 関数の呼び出しと戻りを積み下ろしで管理する領域
- ※3 TLB — 仮想アドレスから物理アドレスへの変換表のキャッシュ
- ※4 ミューテックス/セマフォ/条件変数 — 共有資源への出入りを制御する同期の道具
コンテキストスイッチの本当のコスト
アニメーション『プロセスとスレッド』を開く
CPUが処理対象を切り替えるとき、水面下では次が起きます。
- レジスタの退避 — 実行中のスレッドのCPUレジスタ(途中経過)をメモリへ保存する
- 次の状態の復元 — 切り替え先のレジスタを積み直す
- (プロセス切り替えなら)アドレス空間の交換 — ページテーブルを差し替える
- これに伴い TLB ※3 が無効化され、変換表を引き直すことになる
- さらに キャッシュが冷える — 前のプロセスのデータが追い出され、しばらく遅い
スレッド切り替えは1〜2で済みますが、プロセス切り替えは3〜5の 間接コスト が重い。だから「スレッドは軽い」と言われます。ただし、この直接的な保存時間よりも、キャッシュとTLBが温まり直すまでの見えない遅さの方が、しばしば大きいのです。
スレッドモデルと同期プリミティブ
スレッドを誰が管理するか で、モデルが分かれます。
- 1対1(カーネルスレッド) — 1つのユーザースレッドに、OSが管理する1つの実行単位を対応させる。OSが直接スケジューリングし、複数コアへ素直に散らせるが、生成と切り替えにカーネルを通すコストが乗る
- M対N(ユーザースレッド) — 多数のユーザースレッドを、少数のカーネルスレッドに載せ替えて動かす。切り替えがユーザー空間内で完結して軽いが、実装が複雑で、1つがカーネル内でブロックすると巻き添えが出やすい
共有を安全に使うための道具が 同期プリミティブ ※4 です。
- ミューテックス — 「一度に1人だけ」。鍵を持つ者だけが共有領域に入れる(排他)
- セマフォ — 「同時に入れるのはN人まで」。数を数えて入場制限する
- 条件変数 — 「ある条件が整うまで眠り、整ったら起こしてもらう」。無駄な繰り返し確認を避ける
デッドロックの4条件とメモリ可視性
デッドロック は、次の4つが すべて揃った ときだけ起きます。
- 相互排他 — その資源は同時に1つしか使えない
- 保持と待機 — 何かを持ったまま、別の何かを待つ
- 横取り不可 — 持っている資源を無理やり奪えない
- 循環待ち — AはBを、BはAを……と待ちの輪ができる
裏を返せば、どれか1つを崩せば防げる。「ロックを取る順番を全員で固定する(循環を作らせない)」がよく効くのは、この4番目を壊しているからです。
もう1つの危うさが メモリ可視性 です。あるスレッドが書いた値が、別のスレッドやコアから すぐ見えるとは限りません。各コアはキャッシュを持ち、CPUは命令を並べ替えて先へ進むためです(詳しくはCPUのメモリオーダリング)。だから「共有変数を書いた/読んだ」だけでは足りず、同期プリミティブやメモリバリアで順序と見え方を約束させる 必要があります。競合状態がやっかいなのは、たいてい正しく動いてしまい、ごく稀な巡り合わせでだけ壊れるからです。
非同期・イベントループとの対比
「危うい共有」を避けるもう1つの道が、スレッドを増やさない 発想です。
アニメーション『イベントループ(単一メインスレッド)』を開く
イベントループは、単一のメインスレッド が「やることの列」を順に片づけ、時間のかかる入出力は裏へ投げて、終わった通知だけを受け取ります。共有メモリを複数スレッドで奪い合わないので、ロックが要らず、競合の悩みが根本から消えます。
- マルチスレッド — 本当に同時に計算できる(CPUをたくさん使う仕事に強い)。ただし共有と同期の難しさを背負う
- イベントループ — 待ちの多い仕事(通信・ファイル)を1スレッドで捌く。単純で安全だが、重い計算でメインを塞ぐと全部止まる
どちらが上ではなく、仕事が「計算で詰まる」か「待ちで詰まる」か で選びます。多くの実システムは、両者を組み合わせて使います。
⚠️ 深部の落とし穴
- 偽共有(false sharing) — 別々の変数が同じキャッシュ行に同居し、片方を書くと他方も無効化されて無用に遅くなる
- ロック順序の不一致 — 取る順番がスレッドごとに違うと、循環待ち=デッドロックへ
- 可視性の見落とし — 同期なしの共有変数は、別コアから古い値に見えることがある
- スレッド過多 — 増やすほどコンテキストスイッチとメモリ(各自のスタック)がかさみ、かえって遅くなる
- メインスレッド専有 — イベントループで重い計算を回すと、待ちの列全体が固まる
関連する知識
理解度チェック
そのまま解けます(成績は保存されません)。無料アカウントを作ると、学習の記録と進捗の山登りが始まります。
問1. プロセスの切り替えが、同一プロセス内のスレッド切り替えより重くなりやすいのはなぜ?
問2. デッドロックの成立に必要な4条件に含まれるのはどれ?
問3. 同一プロセス内のスレッドどうしが「各自別々に持つ」のはどれ?
問4. 「同時に入れるのはN人まで」と数を数えて入場制限する同期の道具はどれ?
問5. あるスレッドが共有変数に書いた値が、別コアのスレッドからすぐ見えるとは限らない。主な理由は?
問6. 待ちの多い入出力中心の仕事に、イベントループ(単一メインスレッド)が向く主な理由は?
問7. 別々の変数がたまたま同じキャッシュ行に同居し、片方を書くと他方も無効化されて無用に遅くなる現象を何という?
問8. 多数のユーザースレッドを、少数のカーネルスレッドに載せ替えて動かすスレッドモデルを「◯対◯」の形で何という?