TL

排他制御とデッドロック

複数の処理が同じデータを同時にいじると壊れる。それを防ぐのが排他制御(ロック)で、ロックの掛け方を誤ると今度は全員が固まるデッドロックに陥る。

中級排他制御ロックデッドロック並行処理最終更新: 2026-06-04
TL;DR要点だけ先に
  • 1.複数のスレッド/プロセスが共有データを同時に触ると競合状態(レースコンディション)が起き、結果が壊れる。
  • 2.守りたい範囲(クリティカルセクション)をロック/ミューテックス/セマフォで「同時に1つだけ」に制限するのが排他制御。
  • 3.ロックの掛け方を誤ると互いに待ち合って固まるデッドロックに。成立4条件のどれか1つを崩せば防げる。

なぜ排他制御が要る?──競合状態(レースコンディション)

複数の処理が同じメモリを共有していると、実行の順番(タイミング)次第で結果が変わってしまう ことがあります。これが競合状態(レースコンディション)です。

典型例が「カウンタを 1 増やす」だけの操作。コード上は1行でも、CPUの中では 読む → 足す → 書く の3ステップに分かれます。

counter++ は実際にはこの3手:
  1. counter の値をレジスタに読む  (read)
  2. レジスタの値に 1 を足す        (modify)
  3. 結果を counter に書き戻す       (write)

スレッドAとBが両方 counter++(初期値0)を実行すると、運が悪いとこう絡みます。

時刻  スレッドA            スレッドB            counter
 t1   read(0)                                   0
 t2                        read(0)              0   ← Aの加算前を読んでしまう
 t3   add → 1                                   0
 t4                        add → 1              0
 t5   write(1)                                  1
 t6                        write(1)             1   ← 2回足したのに1!

2回足したのに結果は 1。これを 更新の喪失(lost update) と呼びます。怖いのは、たいてい正しく動いてしまい、ごく稀にしか壊れない こと。テストをすり抜け、本番の高負荷時だけ再現する——並行バグが「再現しないバグ」の代表格なのはこのためです。

「1行だから安全」は誤解

ソース上で1行でも、機械語では複数命令に分かれていれば 途中で割り込まれる(別スレッドに切り替わる)余地があります。安全に「分割不能(アトミック)」と言えるのは、CPUやライブラリがそう保証している操作だけ。見た目の行数とアトミック性は無関係です。

クリティカルセクションとロック

競合が起きる「共有データを触る危険な区間」を クリティカルセクション と呼びます。ここを 同時に1つの処理しか実行できない(相互排他=mutual exclusion) ようにすれば、競合は防げます。

そのための最も基本的な道具が ロック です。クリティカルセクションに入る前に鍵を取得(lock / acquire)し、出るときに返す(unlock / release)。鍵が取られている間、他の処理は待たされます。

lock(mtx)          // 鍵を取る(誰かが持っていたら待つ)
  counter = counter + 1   // ← クリティカルセクション(1人だけ)
unlock(mtx)        // 鍵を返す(待っている誰かが入れる)

こうすれば先ほどの「read → add → write」が 割り込まれずにひとまとまりで実行 され、更新の喪失が起きません。

ロックは必ず解放する

クリティカルセクションの中で 例外/エラーで早期 return したり、解放を書き忘れたりすると、鍵を持ったまま消えて 他の全員が永久に待つ ことになります。try/finally での解放、C++ の RAII(lock_guard)、Go の defer mu.Unlock()、Rust の所有権ベースの Mutex(スコープを抜けると自動解放)など、取りこぼし防止の仕組み を使うのが鉄則です。

ミューテックスとセマフォ

排他制御の代表的な部品が ミューテックス(mutex)セマフォ(semaphore) です。名前が似ていて混同されがちですが、役割が違います。

  • ミューテックス:鍵は1本。「同時に1つだけ」を保証する(相互排他=mutual exclusion の略)。基本的に ロックした本人が解放する(所有者の概念がある)。
  • セマフォ:内部に カウンタ を持ち、「同時にN個まで」を許す仕組み。acquire で1減らし(0なら待つ)、release で1増やす。所有者の縛りはなく、ある処理が acquire、別の処理が release してもよい。

カウンタ上限を 1 にしたセマフォ(バイナリセマフォ)はミューテックスに似ますが、「所有者が解放する」という制約がない点が異なります。用途も、ミューテックスが 資源の保護(クリティカルセクションを守る)なのに対し、セマフォは 資源の個数管理処理の順序づけ(シグナル通知) に向きます。

観点ミューテックスセマフォ
許す同時実行数1つだけ(相互排他)N個まで(カウンタで管理)
所有者の概念ロックした本人が解放誰が release してもよい
主な用途共有データの保護資源数の制限・順序づけ・通知
典型例カウンタや構造体の更新コネクションプール(同時5接続まで等)
上限1のとき本来の使い方バイナリセマフォ(ロック的にも使える)
関連する用語も整理
  • スピンロック:待つときにスレッドを眠らせず、取れるまで ループで確認し続ける(spin)。切り替えコストが惜しい短時間の待ちに向くが、長く回すとCPUを無駄食いする。
  • リーダー/ライター(RW)ロック読むだけなら複数同時にOK、書くときだけ排他。読み取りが圧倒的に多い場面で並行性を上げられる。
  • 条件変数:「ある条件が整うまで眠り、整ったら起こしてもらう」待ち合わせの仕組み。ミューテックスとセットで使う。

アトミック操作:ロックを使わない選択肢

ロックを取らずに競合を防ぐ手もあります。CPUが提供する アトミック操作 の利用です。アトミック(atomic=それ以上分割できない)とは、「読む→変える→書く」が他から割り込まれない1つの不可分な操作 として実行されること。

代表が CAS(Compare-And-Swap / 比較交換)。「今の値が期待どおりなら新しい値に書き換える。違っていたら何もしない」を1命令でやります。

CAS(addr, expected, new):
  もし *addr == expected なら *addr = new として成功
  そうでなければ失敗(誰かが先に書き換えた)
→ これら全体が割り込まれない(アトミック)

CAS を「成功するまで再試行」する形にすると、ロックなしで(lock-free)安全なカウンタやキュー が作れます。ロック待ちで眠らせる必要がなく、デッドロックも原理的に起きにくいのが利点。一方で正しく書くのは難しく、ABA問題(値がA→B→Aと戻ると「変わっていない」と誤認する)などの落とし穴もあります。

使い分けの目安
  • 単純な値の更新(カウンタ、フラグ、参照の差し替え)→ アトミック変数 / CAS が軽くて速い。
  • 複数の変数をまとめて整合的に更新したい → ロック が素直で安全(アトミック1発では守りきれない)。 迷ったらまずロックで正しく書き、ホットパスで詰まってから最適化として lock-free を検討する、で十分です。

デッドロック:守りすぎて全員が固まる

ロックで安全にしたつもりが、今度は 互いに相手の鍵を待ち合って、誰も先に進めなくなる ことがあります。これが デッドロック(deadlock) です。

古典的なのが「ロックを取る順番の食い違い」。送金処理で、口座AとBを両方ロックしたいとします。

スレッド1: lock(A) → (Bを取りたい)→ lock(B)
スレッド2: lock(B) → (Aを取りたい)→ lock(A)

t1: スレッド1が A を確保
t2: スレッド2が B を確保
t3: スレッド1は B を待つ(2が持っている)
t4: スレッド2は A を待つ(1が持っている)
→ 互いに永久に待つ=デッドロック

デッドロック成立の4条件(Coffman条件)

デッドロックは、次の 4つが同時に成り立つとき に発生します。逆に言えば、どれか1つでも崩せば防げます

条件意味崩す対策の例
相互排他資源は同時に1つしか使えない資源を共有可能にする(読み取り専用化など。崩しにくい)
保持と待機鍵を持ったまま別の鍵を待つ必要な鍵を最初に全部まとめて取る/取れなければ手放す
横取り不可(非プリエンプション)他人が握る鍵を奪えない一定時間で諦めて持っている鍵を解放しリトライ
循環待ち待ち関係が輪を描く(A→B→A)全ロックに順序を決め、必ずその順で取得する

実務で一番効くのは 「循環待ち」を崩す こと。ロックに グローバルな取得順序(例:口座IDの小さい方から取る)を決め、全員がその順で取る ようにすれば、輪が閉じず先の送金例も解消します。

ルール: ID が小さい口座から先にロックする
スレッド1(A→B送金): lock(min(A,B)) → lock(max(A,B))
スレッド2(B→A送金): lock(min(A,B)) → lock(max(A,B))
→ 両者とも同じ順で取るので、循環待ちが起きない
ロック取得順は「どこでも同じ」に統一

デッドロックの典型原因は、ある関数が「Aの次にB」、別の関数が「Bの次にA」と バラバラの順序でロックを取る こと。複数ロックをまたぐ場合は、コードベース全体で取得順序を一本化 するのが最も確実な予防策です。ネストするロックは可能なら設計で減らしましょう。

デッドロックと紛らわしい状態

「進まない」系の障害はデッドロックだけではありません。区別できると原因の切り分けが速くなります。

状態何が起きているか進行は?
デッドロック互いに資源を待ち合って輪になり、誰も動けない完全に停止(自力では解けない)
ライブロック譲り合い等で状態は変わるが前に進まない動いているが仕事が進まない
スターベーション(飢餓)優先度等で特定の処理だけ鍵を取れない他は進むが、その処理だけ進めない
ロックの掛けすぎは別の問題を生む

「とにかく全部ロックで囲めば安全」は半分正解で半分間違い。広すぎる範囲を1本の鍵で守ると、並行に動けるはずの処理まで直列化され、せっかくのマルチコアが活きません(スケールしない)。クリティカルセクションは 「本当に共有データを触る最小限」 に絞るのが原則。ここは プロセスとスレッド の理解とセットで考えると腑に落ちます。

まとめ

まとめ

共有データを複数で同時にいじると壊れる(競合状態)。守りたい区間(クリティカルセクション)を ロック / ミューテックス / セマフォ で「同時に1つ(またはN個)」に制限するのが 排他制御。単純な更新なら アトミック操作(CAS) でロックなしにもできます。一方、ロックの取り方を誤ると デッドロック——でも 4条件のどれか1つ(実務では取得順序の統一で「循環待ち」)を崩せば防げる。「正しく守る」と「守りすぎない」の両立がコツです。スレッドそのものの基礎は プロセスとスレッド、CPUがどう切り替えるかは スケジューリング、待ち時間の扱い方は 同期処理と非同期処理 も合わせてどうぞ。

OS Article

排他制御とデッドロックを実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

排他制御

比較で見る軸

難易度: intermediate / カテゴリ: OS / タグ数: 4

導入後に効く点

守りたい範囲(クリティカルセクション)をロック/ミューテックス/セマフォで「同時に1つだけ」に制限するのが排他制御。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
intermediate
カテゴリ
OS
タグ数
4

判断チェックリスト

  • 自社の用途が「排他制御 / ロック」に近いか確認する。
  • 強みである「複数のスレッド/プロセスが共有データを同時に触ると競合状態(レースコンディション)が起き、結果が壊れる。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

排他制御ロックデッドロック並行処理排他制御ロックデッドロック並行処理
参考: 公式情報