排他制御とは、取り合いで数が合わなくなる理由と Mutex

回 3-4 道 3 コンピュータの中

排他制御は、複数の処理が同じデータを同時に書き換えないように、1 度に 1 つの処理だけが触れる順番を作る仕組みです。数に 1 を足す処理は「読む、足す、書く」の 3 手に分かれ、2 本の流れが交互に進むと、並び方 20 通りのうち結果が正しく 2 になるのは 2 通りだけです。鍵 (Mutex) を取ってから 3 手を進め、終わったら放すと、どの並び方でも結果は正しくなります。

同時に動く 2 つの処理の並び方を数え、数が合わなくなる並びと、鍵で防げる理由を切り分けます。

同時に足すと、なぜ数が合わなくなるのか

数が合わなくなるのは、1 を足す処理が、実は「読む、足す、書く」の 3 手に分かれていて、2 つの処理の手が交互に混ざるからです。混ざらないように 1 度に 1 つだけ通す仕組みが、排他制御 (はいたせいぎょ、同じ物を同時に触らせない決まり) です。

いいねの数が 0 の投稿に、A さんと B さんがほとんど同時にいいねを押しました。サーバーでは 2 人分の処理が同時に動き、それぞれ数に 1 を足します。2 になるはずが、ときどき 1 にしかなりません。手元で 1 人ずつ押すと 2 になります。この「たまにだけ合わない」不具合は、どこで起きているのでしょうか。

この回では、2 つの処理の進む順番を全部数えて、数が合わなくなる並び方を確かめ、鍵 (Mutex) で直ることも確かめます。

排他制御はどう働くのか

排他制御は、共有の物を触る区間に鍵をかけ、鍵を持つ処理だけがその区間を進めるようにして働きます。ここでは、同時に動くスレッドやプロセス (動いているプログラム 1 つ分) をまとめて「流れ」と呼びます。

足し算は 3 手に分かれる

n = n + 1 は 1 行ですが、コンピュータの中では次の 3 手に分かれます。

  1. 読む (いまの n を、自分の手元に写す)
  2. 足す (手元の値に 1 を足す)
  3. 書く (手元の値を n に戻す)

Python の公式の FAQ も、i = i+1 を不可分 (途中で割り込まれない) ではない操作の例に挙げています。

A が読んで、まだ書かないうちに B も読むと、2 つの流れはどちらも 0 を読みます。どちらも 1 を書くので、結果は 1 です。B の足した分は、A の書いた値で上書きされて消えます。

流れの進む順番で結果が変わる状態を、競合状態 (レースコンディション) と呼びます。共有の物を触る区間 (ここでは読むから書くまで) はクリティカルセクションと呼ばれ、Wikipedia は排他制御を、この区間に複数の流れが同時に入るのを防ぐこと、と言い換えています。

並び方は何通りあるのか

各流れの中の順番は崩さずに 2 つの流れの 3 手を混ぜると、並び方は 20 通りあります。下のコードで全部を数えた結果は次の通りです。

流れの数並び方正しい結果になる並び方
2 本20 通り2 通り (結果 2)
3 本1680 通り6 通り (結果 3)

正しい結果になるのは、1 本が 3 手を全部終えてから次の 1 本が読み始める並び方だけです。流れが増えるほど、正しい並び方の割合は小さくなります。

本番で毎回は壊れないのは、流れの切り替えが 3 手の途中に来たときだけ壊れるからです。だから手元で再現しにくくなります。

並び方が何通りあるか

図のつまみで流れを 1 本にすると、並び方は 1 通りになり、壊れようがなくなります。

鍵 (Mutex) で 1 本ずつ通す

鍵 (Mutex、ミューテックス) は、取る (acquire) と放す (release) の 2 つの操作を持つ札です。取れるのは 1 度に 1 つの流れだけで、ほかの流れは、放されるまで取る手で待ちます。読むの前に取り、書くの後に放せば、3 手の途中にほかの流れが入れなくなります。

Python の threading.Lock も、「かかっている」と「かかっていない」の 2 つの状態を acquire() と release() で切り替えます。仕事で書く形は次の通りで、with を使うと区間を抜けるときに必ず放されます。

Python で鍵をかける形

import threading

lock = threading.Lock()
count = 0

def like():
    global count
    # 鍵の中は 1 本ずつ通る
    with lock:
        count = count + 1

鍵のほかに、1 つ足すような決まった小さな操作に限って、CPU が読む、足す、書くの 3 手を割り込めない 1 命令で済ませる仕組み (アトミック命令) もあります。どんな処理でも 1 命令にできるわけではなく、まとまった処理は鍵で守ります。上の図の「アトミック 1 命令」のボタンはこちらで、割り込む隙がそもそもありません。

方法何をするか代わりに払うもの
鍵 (Mutex)区間を 1 本ずつ通す待つ時間
不可分な命令3 手を 1 手にする使える操作が限られる
版を照らす書くときに変わっていないか見る断られたらやり直す

データベースの排他制御

データベースでも、同じ行を 2 人が同時に書き換えると数が合わなくなります。防ぎ方には、先に行へ鍵をかける悲観ロックと、書くときに版を照らす楽観ロックの 2 つがあります。

TERASOLUNA の指針は、在庫 5 個を 2 人が同時に開き、10 個と 20 個を足して書き戻すと、本当の 35 と合わなくなる例を挙げています。上の 3 手と同じ形です。

悲観ロックでは、読むときに SELECT ... FOR UPDATE で行に排他の鍵をかけます。PostgreSQL の公式文書によると、FOR UPDATE の行は、トランザクション (まとめて確定する一続きの処理) が終わるまで、ほかから書き換えも鍵の取得もできません。FOR SHARE は共有の鍵で、ほかの FOR SHARE とは同時に持てます。

更新中の行に札をかけ、同じ行を直したい人だけが札の前で待ち、ほかの行と普通の SELECT は待たないことを示した図

楽観ロックでは、行に「版」の番号を持たせ、読んだときと版が同じときだけ書きます。版が変わっていたら断り、読み直してやり直します。次のコードは、TERASOLUNA の例と同じ数で比べます。

動かす 版を照らす楽観ロック

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

版を照らす False
 B が +10 書けた
 A が +20 書けた
 在庫 25
版を照らす True
 B が +10 書けた
 A が +20 断られた
 やり直す 書けた
 在庫 35

照らさないと B の 10 個が消えて在庫は 25 です。照らすと A の 1 回目は断られ、読み直した 15 に 20 を足して 35 になります。

方法鍵をかける時向く場面
悲観ロック読むとき同じ行の取り合いが多い
楽観ロックかけず、書くときに照らす取り合いがまれ

動かして確かめる

本物のスレッドの代わりに、2 つの流れを決めた順番で 1 手ずつ進める模型を使います。順番を文字列で渡すので、壊れる並び方を狙って何度でも再現できます。

1 本目は、順番を渡して、1 手ごとの手元の値と共有の数を出します。"ABABAB" は A、B、A、B、A、B の順に 1 手ずつ進める意味です。

動かす 3 手を交互に進める模型

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

流れ 手 手元 数
A 読 0 0
B 読 0 0
A 足 1 0
B 足 1 0
A 書 1 1
B 書 1 1
ABABAB の結果 1
AAABBB の結果 2
ABBBAA の結果 1

ABABAB では A も B も 0 を読み、どちらも 1 を書くので結果は 1 です。AAABBB だけが 2 になります。ABBBAA は B が 3 手を終えても、A の手元は古い 0 のままなので、A が 1 を書いて B の分を消します。

2 本目は、並び方を全部たどって結果ごとに数えます。鍵を取る手 (取) と放す手 (放) を入れた版も数えます。鍵が取られていれば、取る手は待ちます。

動かす 並び方を全部数える

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

鍵なし 2 本 20 通り
 結果 1 18 通り
 結果 2 2 通り
鍵なし 3 本 1680 通り
 結果 1 1404 通り
 結果 2 270 通り
 結果 3 6 通り
鍵あり 2 本 2 通り
 結果 2 2 通り
鍵あり 3 本 6 通り
 結果 3 6 通り

鍵が無いと、2 本では 20 通りのうち 18 通りで結果が 1 になります。3 本では 1680 通りのうち、正しく 3 になるのは 6 通りだけです。

鍵を入れると、2 本では 2 通り、3 本では 6 通りしか並び方が残らず、どれも正しい結果です。並び方が「1 本ずつ終える」形だけに減るからで、その分ほかの流れは待ちます。

排他制御はどこで崩れるのか

排他制御は、全員が鍵を守り、取る順番がそろい、鍵を持つ流れが進めるときにだけ効きます。1 つでも崩れると、数が合わないか、全員が止まります。

1 本でも鍵を守らないと、また壊れる

鍵は、取る約束を守る流れどうしでしか効きません。B が鍵を取らずに読めば、A の 3 手の途中に入れます。

2 つの鍵を逆の順で取ると止まる

2 つの流れが、それぞれ相手の持つ鍵を待って、どちらも先へ進めなくなる状態をデッドロックと呼びます。たとえば口座 12 から口座 47 へ送る処理と、47 から 12 へ送る処理が、どちらも「送り元の鍵、送り先の鍵」の順で取ると、A は 12、B は 47 を持ったまま、互いの鍵を待ち続けます。

次のコードは、B が鍵を忘れる場合と、2 つの鍵 (12 と 47) を逆の順で取る場合、そろえた場合を数えます。

動かす 鍵を忘れる、逆の順で取る

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

B が鍵を忘れる 56 通り
 結果 1 48 通り
 結果 2 8 通り
逆の順で取る 5 通り
 止まる 2 通り
 結果 2 3 通り
小さい順にそろえる 4 通り
 結果 2 4 通り

B が鍵を忘れると、56 通りのうち 48 通りで結果が 1 になります。

逆の順で取ると、5 通りのうち 2 通りで止まります。A が 12 を、B が 47 を取ると、どちらも次の鍵を取れないからです。どちらも 12 から取るようにそろえると、止まる並び方は 0 です。

取る順番をそろえる

PostgreSQL の公式文書も、行の鍵でデッドロックが起きうること、起きたら見つけて片方のトランザクションを中止すること、いちばんの守りはどの処理も同じ順番で鍵を取ることだと書いています。

鍵を持ったまま進めないと、待つ側も止まる

鍵を持つ流れが、ほかの仕事に押されて先へ進めないと、鍵を待つ流れも一緒に止まります。PostgreSQL の公式文書も、鍵を待つ処理は放されるまで待ち続けるので、利用者の入力を待つ間にトランザクションを開けておくのは良くないと注意しています。鍵の中は短く保ちます。

排他制御は仕事でどう確かめるのか

仕事では、読んでから書くまでにほかの処理が入れないかをコードで探し、入れるなら鍵か版の照らし合わせを足します。

  1. 共有の値を読んでから書くまでの間に、待ち (await、ネットワーク、利用者の入力) がないか探す。あれば、その間にほかの処理が入れる
  2. データベースなら、読んだ値で書き戻す前に、SELECT ... FOR UPDATE で行に鍵をかけるか、版の列を照らす
  3. 鍵を 2 つ以上取る所では、取る順番を 1 つの決まり (id の小さい順など) にそろえる
  4. 楽観ロックで断られたときと、デッドロックで中止されたときに、やり直す作りになっているかを確かめる

本物のスレッドの試験では、壊れる並び方を狙って出せません。上の模型の数え方なら、次のように試験に固められます。

並び方を数えて固める試験

def test_key_keeps_count():
    pg = {w: key for w in "ABC"}
    out = Counter()
    pc = dict.fromkeys(pg, 0)
    ex(pg, {"n": 0}, pc, out)
    # どの並び方でも 3 になる
    assert out == {"結果 3": 6}

関わる実例

Mars Pathfinder の再起動 (1997年)

1997年7月4日に火星に着いた Mars Pathfinder は、数日後からコンピュータの再起動をくり返しました。

ソフトウェアの責任者だった JPL の Glenn Reeves の 1997年12月15日の記録によると、気象を測る優先度の低い ASI/MET の処理が、select() の中の排他用のセマフォ (鍵) を持ったまま、中くらいの優先度の処理に割り込まれ続けました。優先度の高い bc_dist はその鍵を待って止まり、次の周期を準備する bc_sched が「bc_dist が終わっていない」と見つけて再起動しました。優先度の逆転と呼ばれる形です。

直し方は、鍵の設定で優先度の継承 (鍵を持つ低い処理に、待つ高い処理の優先度を一時的に渡す) を有効にすることでした。地上の試験室では 18 時間足らずで再現でき、打ち上げ前の試験は最も良い場合のデータ量に限られていたと Reeves は書いています。

崩れた前提は、「鍵を持つ処理は、すぐに鍵を放す」です。鍵を持つ処理が走らせてもらえなければ、待つ側も止まります。

出典は What really happened on Mars? (Glenn Reeves、JPL) です。着陸の日付は下の一次資料の Mike Jones の記録によります。

Linux の Dirty COW (CVE-2016-5195、2016年)

米国の脆弱性データベース NVD に2016年11月10日に載った CVE-2016-5195 は、Linux カーネルの mm/gup.c の競合状態です。4.8.3 より前の 2.x から 4.x のカーネルで、書き込むときに写しを作る仕組み (copy-on-write) の扱いを誤るため、ローカルの利用者が読み取り専用のメモリに書き込み、権限を上げられました。記録によると、2016年10月に実際の攻撃で使われ、Dirty COW と呼ばれています。

崩れた前提は、「確かめてから書くまでの間に、ほかの流れは入らない」です。3 手の足し算と同じ隙が、ここでは権限を越える入口になりました。

出典は CVE-2016-5195 (NVD) です。

よくある問い

排他制御とロックは何が違いますか

排他制御は「同じ物を同時に触らせない」という目的で、ロック (鍵) はそれを実現する道具の 1 つです。ロックのほかに、不可分な命令や、版を照らす楽観ロックも排他制御の手段です。

排他制御は英語で何と言いますか

英語では mutual exclusion と言います。Mutex はこの 2 語を縮めた呼び名で、Mars Pathfinder の記録も「mutual exclusion locks (mutexes)」と書いています。

Mutex とセマフォは何が違いますか

Mutex は 1 度に 1 つの流れだけを通す鍵で、取った流れ (持ち主) が放します。POSIX の pthread_mutex_lock の仕様は、ほかのスレッドが取っている間は呼んだスレッドを待たせると定め、持ち主でない流れが放したときは、種類によってエラーを返すか、動きを決めていない (未定義) としています。セマフォは 0 より小さくならない整数で、Linux の sem_overview(7) によると、1 増やす sem_post と 1 減らす sem_wait の 2 つの操作を持ち、値が 0 のときの sem_wait は 0 より大きくなるまで待ちます。Dijkstra は原典の EWD123 でこの 2 つの操作を P と V と名付け、値を 0 と 1 に限ったセマフォだけで排他を組めることを示しました。値を 1 にすると Mutex に近い働きになりますが、セマフォには持ち主が無く、取っていない流れも値を増やせる点が違います。

Python には GIL があるのに鍵は要りますか

要ります。Python の公式の FAQ によると、GIL (同時に 1 つのスレッドだけに Python を動かさせる仕組み) があっても、流れの切り替えはバイトコードの間で起き、i = i+1 は不可分ではありません。また Python 3.13 からは、GIL を外して動かす版も用意されています。

悲観ロックと楽観ロックはどう選びますか

同じ行の取り合いが多いなら悲観ロック、まれなら待ちの少ない楽観ロックが向きます。楽観ロックでは、断られたあとに読み直してやり直す作りが要ります。

確認クイズ

答えだと思う選択肢を押すと、その場で正誤と理由が開きます。4 問すべてに正しく答えると、この回は修了で、石が緑に変わります (この端末のブラウザに残ります)。間違えた問いは、あとから解き直せます。

1 問目

上の「3 手を交互に進める模型」の run を使います。このコードを動かすと何が出ますか。

AABBBA の順で進める

n, log = run("AABBBA")
print(n)

2 問目

鍵の無い 3 本の流れが 3 手ずつ進むとき、並び方は 1680 通りあります。そのうち結果が正しく 3 になるのは何通りですか。

3 問目

A は鍵 12、47 の順に、B は鍵 47、12 の順に取ります。ときどき両方が止まります。直し方として正しいのはどれですか。

4 問目

楽観ロックで、A と B が同じ版 1 の行を開き、B が先に書きました。A が版 1 のまま書こうとすると、どうなりますか。

説明課題

いいねの数が 0 の投稿に、2 人がほとんど同時にいいねを押したら、数が 1 にしかならないことがありました。なぜ 1 になるのかを「読む、足す、書く」の 3 手と並び方の言葉で説明し、鍵 (Mutex) を入れるとなぜ直るのかを 3〜4 文で書いてください。書いたら、下の模範解答を開いて比べてください。

模範解答

自分の説明を書いてから開いて、比べてください。

この回の先の一歩

在庫や残高、いいねの数のように、たくさんの人が同時に書き換える値を扱う画面や API を作るとき、どこに鍵や版の照らし合わせが要るかを見抜く力になります。

もっと深く

この回の続きを、ゆめさくのコースの章や道具で深められます。頁の中の確認クイズに答えたあと、手を動かす量を増やしたいときに使ってください。

一次資料

どれも 2026年10月27日に開いて、題と中身を確かめました。

次へ

道 3 コンピュータの中の回

  1. 3-1OS の役目とシステムコールOS が間に立つ理由頁で読む
  2. 3-2プロセスとスレッドスケジューラ、プロセス間通信頁で読む
  3. 3-3メモリの階層とキャッシュ局所性、当たりと外れ、L1 から主記憶まで頁で読む
  4. 3-4取り合いと排他レースコンディション、Mutexこの回
  5. 3-5CPU が命令を実行する命令、レジスタ、クロック頁で読む

道 3 の頁へ戻るほかの道を見る