Mímisbrunnr知恵の泉

← コンピュータ基礎 一覧

🎓 レベル:標準 | 重要度:A(必須)

📎 前提:競合状態とクリティカルセクション | 関連:デッドロック並行性のパターン

要点(BLUF)

概念 ── 守る道具の全体像

クリティカルセクションを「同時に1つだけ」にするには、入口で印を立て、出口で外す仕組みが要ります。これがロック。

flowchart TB
    subgraph MX["ミューテックス(N=1:1人だけ)"]
      m["lock -> 区間 -> unlock"]
    end
    subgraph SE["セマフォ(N個まで)"]
      s["acquire(カウンタ--)-> 資源 -> release(カウンタ++)"]
    end

仕組み① ── アトミック命令が土台

ここに鶏と卵の問題があります。「ロックを取る」操作自体が「フラグを読んで、空いていれば立てる」という read-modify-write で、これが競合したら(競合状態とクリティカルセクション)ロックの意味がありません。

解決はソフトでなくハードにあります。CPUは「読みと書きを不可分に行う命令」を提供します。

これらは1命令でCPUがバスをロックして実行するので、途中で他コアが割り込めません。このハードの不可分性の上に、すべての高級な同期(ミューテックス・セマフォ・ロックフリーデータ構造)が積み上がります。「最終的な正しさはハードが保証する」のがポイントです。

仕組み② ── スピン vs ブロック

ロックが取れないとき、待ち方に2系統あります。

flowchart TB
    want["ロックを取りたい"] --> avail{"空いている?"}
    avail -->|"Yes"| got["取得して区間へ"]
    avail -->|"No (スピン)"| spin["ループで再試行(CPUを回し続ける)"] --> avail
    avail -->|"No (ブロック)"| sleep["待機列に入り眠る(CPUを手放す)"]
    sleep -->|"解放時に起こされる"| got

要するに「待ち時間 < 文脈切り替えコスト」ならスピン、それ以上ならブロック。実用のロックは短時間スピンしてから眠るハイブリッドが多い。

具体例 ── ロックで競合を消す

競合状態とクリティカルセクション の壊れたカウンタを、ロックで囲むだけで直ります([[#対応ラボ]] の同じ 04-01_race_and_lock.py)。

lock = threading.Lock()
def add_safe():
    global counter2
    for _ in range(N_ITERS):
        with lock:               # ここがクリティカルセクション(1スレッドのみ)
            tmp = counter2
            counter2 = tmp + 1

実行結果(実機):

== ロック有り ==
期待値 16000 / 実際 16000 / 一致: True

with lock: の中を「同時に1つ」に制限したので、read-modify-writeが割り込まれず、更新喪失が消えました。

仕組みの直観 ── なぜこの設計か

⚠️ よくある誤解・落とし穴

対応ラボ

cs-foundations-study/labs/04-01_race_and_lock.py(ロック有り側を実行し、結果が常に正しくなることを確認済み)。

関連

第4章 並行処理と同期 目次