セマフォとミューテックスの古典的同期問題
セマフォの計数/二値、生産者消費者・読者書込者・食事する哲学者をどう定式化し解くかを原理から整理。ミューテックスとの所有権の違いまで押さえれば、並行設計のバグを構造で潰せる。
- 1.セマフォは内部カウンタを wait(P)で減らし signal(V)で増やす整数。負になった分だけスレッドが眠り、待ち行列で順に起こされる。
- 2.生産者消費者・読者書込者・食事する哲学者は、empty/full のペア、書込ロックと読者カウント、資源獲得順序の対称性破りで解ける。
- 3.ミューテックスは所有者が解放する相互排他専用、計数セマフォは所有権なしの資源数管理・順序づけ。二値セマフォと混同しないこと。
セマフォの原理:カウンタと待ち行列
セマフォ(semaphore)は Dijkstra が 1965 年に提案した同期プリミティブで、本質は 整数カウンタと、それを操作する2つのアトミックな操作 だけです。
- wait(P 操作、acquire、down とも):カウンタを 1 減らす。減らした結果が負なら、呼び出したスレッドをブロックして待ち行列に入れる。
- signal(V 操作、release、up とも):カウンタを 1 増やす。待っているスレッドがいれば 1 つ起こす。
wait(S):
S.count -= 1
if S.count < 0:
このスレッドを S の待ち行列に入れてブロック
signal(S):
S.count += 1
if S.count <= 0: // まだ待っている者がいる
待ち行列から1つ取り出して起こす
ここで重要なのは、カウンタの符号が状態を表す こと。値が正なら「あと何個 wait を通せるか」を、負なら「何個のスレッドが待たされているか」を表します。count -= 1 と判定が 不可分(アトミック) に行われる点が肝で、これを保証するために実装内部ではスピンロックや割り込み禁止で短いクリティカルセクションを守ります。
カウンタの初期値が任意の N を取れるものを 計数セマフォ(counting semaphore)、0 と 1 しか取らないものを 二値セマフォ(binary semaphore) と呼びます。計数セマフォは「同時に N 個まで使える資源」を、二値セマフォは「資源が空いているか否か」を表現します。二値セマフォはミューテックスに似ますが、後述のとおり所有権の概念がない点で異なります。
待ち行列を FIFO にすれば、起こす順序が公平になりスターベーション(飢餓)を避けられます。逆に「ただ起こす」だけの実装だと、毎回同じスレッドが先に走って一部が永久に待たされうる——強いセマフォ(FIFO 保証)と弱いセマフォ の区別はここにあります。
生産者消費者問題(有界バッファ)
固定長 N のリングバッファに、生産者が要素を入れ、消費者が取り出す。守るべき不変条件は2つ——満杯のときに入れない/空のときに取り出さない、そして インデックス更新の競合を防ぐ ことです。これを 2つの計数セマフォと1つの相互排他 で表します。
empty = セマフォ(N) // 空き枠の数
full = セマフォ(0) // 詰まっている要素の数
mutex = セマフォ(1) // バッファ操作の相互排他
生産者: 消費者:
wait(empty) // 空きを確保 wait(full) // 要素を確保
wait(mutex) wait(mutex)
バッファに入れる バッファから取る
signal(mutex) signal(mutex)
signal(full) // 要素+1 signal(empty) // 空き+1
empty と full の 和は常に N に保たれ、片方の wait が他方の signal で必ず釣り合います。これが「満杯/空」を正しくブロックする仕組みです。
生産者で wait(mutex) を wait(empty) より先に書くと、バッファ満杯時に生産者が mutex を握ったまま empty を待ち、消費者は mutex が取れず full を解放できない——互いに待ち合って固まります。資源セマフォ(empty/full)を先、mutex を後 の順序が鉄則です。この「取得順序の食い違い」がデッドロックの典型で、排他制御とデッドロック の循環待ち条件そのものです。
読者書込者問題
共有データに対し、読者は同時に何人でも許すが、書込者は1人だけ・読者とも排他 したい。読み取りが多い場面で並行性を上げる定式化です。素朴な解(第一読者問題)はこうです。
rw_mutex = セマフォ(1) // 書込みの排他(読者集団 vs 書込者)
read_mutex = セマフォ(1) // readers カウンタの保護
readers = 0
読者: 書込者:
wait(read_mutex) wait(rw_mutex)
readers += 1 書き込む
if readers == 1: wait(rw_mutex) signal(rw_mutex)
signal(read_mutex)
読む
wait(read_mutex)
readers -= 1
if readers == 0: signal(rw_mutex)
signal(read_mutex)
ポイントは 最初の読者が rw_mutex を取り、最後の読者が返す こと。読者がいる間は書込者が締め出されます。ただしこの解は読者を優先するため、読者が途切れないと 書込者が永久に待つ(writer starvation)。実務の RW ロックは、待機中の書込者がいたら新規読者をブロックする「書込者優先」や、到着順に処理する公平版で飢餓を防ぎます。
食事する哲学者問題
円卓に N 人の哲学者、隣り合う2人の間に1本ずつフォーク。食べるには 左右2本 が要る。全員が同時に左フォークを取ると、各自が右を待ち続けて 循環待ちのデッドロック に陥る——これが本質です。
| 解法 | やり方 | 崩す条件 |
|---|---|---|
| 資源順序付け | フォークに番号を振り、必ず小さい番号から取る | 循環待ち(輪が閉じない) |
| 非対称化 | 偶数番は左→右、奇数番は右→左の順で取る | 循環待ち(対称性を破る) |
| 同時着席制限 | セマフォで同時に食事できるのを N-1 人までに制限 | 保持と待機(全員同時に握れない) |
| 原子的獲得 | 両フォークが空くまで待ち、揃ったら一括取得 | 保持と待機(片方だけ握らない) |
最も明快なのが 資源順序付け。最後の1人だけ取得順が逆になるため輪が閉じず、デッドロックが原理的に起きません。
左フォーク = i, 右フォーク = (i+1) mod N とすると
哲学者 i: lo = min(左,右), hi = max(左,右)
wait(fork[lo]); wait(fork[hi]) // 全員が「小さい番号から」
食べる
signal(fork[hi]); signal(fork[lo])
食事する哲学者は、デッドロック成立の4条件(相互排他・保持と待機・横取り不可・循環待ち)の どれを崩すか の見本市です。順序付けと非対称化は「循環待ち」を、着席制限と原子的獲得は「保持と待機」を崩します。問題を見たら「どの条件を狙うか」を先に決めると解法が一意に絞れます。
ミューテックスとの本質的な違い:所有権
二値セマフォとミューテックスは「同時に1つだけ」を実現する点で似ますが、所有権(ownership) の有無が決定的に違います。
| 観点 | ミューテックス | 計数/二値セマフォ |
|---|---|---|
| 所有者の概念 | ロックした本人だけが解放できる | 誰が signal してもよい |
| 主な目的 | 相互排他(共有データの保護) | 資源数の管理・順序づけ・通知 |
| 優先度継承 | 所有者が明確なため適用できる | 所有者不在で原理的に不可 |
| 再帰取得 | 再帰ミューテックスなら可 | 概念がない |
| 典型的な誤用 | 他スレッドが unlock → 未定義動作 | 二値セマフォをロック代わりに使い所有権を失う |
所有権があることで生まれる最大の利点が 優先度継承 です。低優先度スレッドがミューテックスを握ったまま高優先度スレッドにプリエンプトされ、中優先度が割り込んで進めなくなる——これが優先度逆転。所有者が一意に決まるミューテックスなら「握っている低優先度スレッドを一時的に昇格させる」対処ができますが、所有者の概念がないセマフォではそもそも誰を昇格すべきか定まりません。詳細は 優先度逆転と優先度継承 を参照してください。
あるスレッドが wait し別のスレッドが signal する 生産者→消費者のような順序づけ・通知 は、所有者が変わるためミューテックスでは表現できず、セマフォ(または条件変数)の出番です。逆に、同じスレッドが取得・解放する クリティカルセクションの保護 はミューテックスが正解。所有権を意識せず二値セマフォをロック代わりに使うと、取り違えやダブル解放を検知できず、優先度継承の恩恵も失います。
Linux では、ユーザー空間の高速な相互排他は futex を土台に実装され、競合がない限りシステムコールを発行せずアトミック命令だけで完結します。カーネル内部の各種ロックは カーネルのロック機構 に、ロックを使わない選択肢は ロックフリーとCAS にまとめてあります。
まとめ
セマフォは カウンタ+待ち行列 が本体で、wait(P)で減らし signal(V)で増やす。負の値の絶対値が待機数を表す。計数セマフォ は資源数を、二値セマフォ は空き有無を管理する。古典的3問題は定式化が決まっていて——生産者消費者 は empty/full のペアと mutex(取得順序に注意)、読者書込者 は書込ロックと読者カウント(飢餓に注意)、食事する哲学者 は資源順序付けや着席制限で4条件のどれかを崩す。そして ミューテックスは所有者が解放する相互排他専用 で優先度継承が効く一方、セマフォは所有権なしの資源管理・順序づけ に向く——この違いを押さえると、保護なのか通知なのかで道具を正しく選べます。並行制御の全体像は 排他制御とデッドロック も合わせてどうぞ。
OS Article
セマフォとミューテックスの古典的同期問題を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
セマフォ
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 5
導入後に効く点
生産者消費者・読者書込者・食事する哲学者は、empty/full のペア、書込ロックと読者カウント、資源獲得順序の対称性破りで解ける。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 5
判断チェックリスト
- 自社の用途が「セマフォ / ミューテックス」に近いか確認する。
- 強みである「セマフォは内部カウンタを wait(P)で減らし signal(V)で増やす整数。負になった分だけスレッドが眠り、待ち行列で順に起こされる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。