並行制御と同期 — 同時アクセスを安全にする

複数のスレッドが同じデータを同時にいじると、何が起きるのか。答えが静かに壊れる競合状態、それを直すロック、そしてロックが招くデッドロック — 並行プログラムの落とし穴を、実行の順番を1手ずつ動かしながら目で確かめます。

1. 答えが静かに壊れる — 競合状態

「変数を1増やす」という一見アトミックな操作も、CPU の中では3つの手順に分かれています — ①読む → ②+1する → ③書き戻す。1つのスレッドがこの途中で止められ、別のスレッドに割り込まれると、更新が上書きで消える。これが競合状態(レースコンディション)です。

下で2つのスレッドが同じ counter を +1 します。正しい答えは当然 2。でも「危険な交互実行」を選んで1手ずつ進めると、答えが 1 になる瞬間が見えます。

2つのスレッドが counter を +1 — 順番次第で答えが変わる
水色=スレッド1、桃色=スレッド2。▶ が次に実行される手順。各スレッドは自分のレジスタに読み込んでから書き戻します。交互実行だと両方が同じ 0 を読んでしまい、あとの書き込みが前の書き込みを上書きして消すのが下段の時系列で分かります。
POINT — アトミック性(不可分性)が鍵 読み・加算・書き戻しが3つに割れているから割り込まれる。この3手順を「分割できない1つの塊」=クリティカルセクションとして守れば競合は起きない。ハードウェアには compare-and-swap のような不可分命令があり、その上にロックが作られている。
起こりうる実行順序 = (m + n)! / (m! · n!) m,n=各スレッドの手順数。3手順×2スレッドでも 20通り。スレッドや手順が増えると爆発し、テストで悪い順序を毎回引き当てるのは不可能に近い。だから「たまに壊れる」バグになる。

2. 1人ずつ入れる — ロック / ミューテックス

競合を防ぐ王道がロック(ミューテックス)。クリティカルセクションに「一度に1スレッドしか入れない」という鍵をかけます。鍵が空くまで他のスレッドはドアの前で待つ(=直列化)。すると読み書きが割り込まれず、答えが必ず正しくなります。

下では複数のスレッドが休みなく counter を +1 し続けます。ロックを外すと全員が同時に部屋へ入り、更新が衝突して消えていく(実際の値が期待値に届かない)。ロックを掛けると1人ずつ入り、ぴたりと一致します。

クリティカルセクションを鍵で守る — ロックあり / なし
中央の部屋=クリティカルセクション。ロックありなら誰かが入っている間、他はドア前で待機(🔒)。ロックなしだと複数が同時侵入し、赤い×=更新の消失が発生。右上の「実際 / 期待」がずれ、消失が積み上がります。
注意 — ロックはただ飯ではない ロックの範囲(粒度)が粗いと待ち行列ができ並列性が落ちて遅くなる。細かすぎると掛け忘れやバグの温床に。そして複数のロックを持ち始めた瞬間、次節のデッドロックという新しい罠が口を開ける。

3. 待ち合わせる — セマフォと生産者・消費者

ロックが「1人だけ」の鍵なら、セマフォは「あとN個空いている」を数える信号機です。代表例が生産者・消費者問題 — 作る側と使う側が有限のバッファを挟んで協調します。バッファが満杯なら生産者が待ち、空なら消費者が待つ。この待ち合わせをセマフォが自動でさばきます。

下で生産と消費の速度を変えてみてください。生産が速すぎるとバッファが埋まって生産者がブロックされ(=バックプレッシャー)、消費が速すぎると空になって消費者が止まります。

有限バッファでの待ち合わせ — セマフォが空きと中身を数える
左=生産者、右=消費者、中央=リングバッファ(FIFO)。緑のマスが中身入り。空きと中身の2つのセマフォが残数を管理し、満杯・空になった側を自動でブロックします(黄色の「待機」)。
POINT — セマフォ = 数える信号機 ミューテックスは資源1個の鍵。セマフォはN個の許可証を持ち、取得(P/wait)で1減らし、返却(V/signal)で1増やす。0になった人は自動で待たされる。「空きスロット数」と「中身の個数」を2つのセマフォで数えるだけで、生産者と消費者がきれいに歩調を合わせられる。

4. 全員が永遠に止まる — デッドロック

複数のロックを扱うと生まれる最悪の事態がデッドロック。スレッドAがロック1を持ったままロック2を欲しがり、スレッドBはロック2を持ったままロック1を欲しがる。互いに相手の持ち物を待ち、誰も一歩も進めなくなる — 交差点で全員が譲らずに固まった状態です。

下の「デッドロックを起こす」を押すと、AとBがすれ違いにロックを掴み、循環待ちの輪が真っ赤に浮かびます。「安全な順序」なら二人とも同じ順(1→2)で掴むので輪ができず、交代で仕事を回し続けます。

循環待ちで全員停止 — デッドロックの発生と回避
丸=スレッド、四角=ロック。実線=「保持中」、破線=「待っている」。すれ違いに掴むと A→ロック2→B→ロック1→A の輪が閉じ、両者が永久停止。安全な順序なら輪が閉じず、片方が終えてから相手が進みます。
POINT — デッドロックの4条件(Coffman) ①相互排除(資源は同時に1人)②保持と待機(持ったまま次を待つ)③横取り不可(無理やり奪えない)④循環待ち(待ちの輪)。この4つが同時に揃うと発生する。逆に1つでも崩せば防げる — 例えばロックの取得順序を全スレッドで統一すれば ④循環待ち が消える。

5. まとめ — 同時に触るなら、約束事がいる

一歩先へ — ロックを使わない世界 ロックの難しさから逃れる潮流もある。ロックフリー / ウェイトフリーなデータ構造は compare-and-swap を土台に「待たせずに」協調する。トランザクショナルメモリは「まとめて実行し、衝突したらやり直す」DBのような発想。そして共有しなければ競合しないという割り切りが、メッセージパッシング(Go の channel)やアクターモデル(Erlang / Elixir)。並行の正しさは「速さ」ではなく「約束の設計」で決まる。