コンピューターサイエンス:アルゴリズム / OS / ネットワーク / DB

レースコンディション

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

レースコンディション

このレッスンで分かること

  • レースコンディションは「複数のスレッド/プロセスが 同じ資源を同時に変更 し、実行順次第で結果が変わるバグ」です
  • シングルスレッドでは決して起きず、並行性を導入した瞬間に頭をもたげます
  • 解決には アトミック操作ロック(mutex、セマフォ等) が必要です

レースコンディション とは

レースコンディション。本レッスンでは、レースコンディション の基本から実際の使いどころまでを整理し、現場で迷わず使える形に落とし込みます。

レースコンディションを防ぐ 3 つのアプローチ

アプローチ仕組み速度適用範囲
アトミック命令CPU 命令 1 つで読み・変更・書き戻し完結プリミティブ型のみ
ロック (mutex / spinlock)入室前に獲得、退室時に解放任意の処理
ロックフリー / Wait-FreeCAS 等で順序を工夫設計が難しい、メモリオーダリング注意

代表的なバグパターンとして TOCTOU (Time of Check to Time of Use) があります。access() でチェック → open() で使用、の間にすり替えられる典型的な脆弱性です。

何が起こっているか

「コンテキストスイッチで処理が割り込まれる」という事実が、共有データの更新を危険にします。下記コードを 2 スレッドで動かすと、足し算 1 回が 3 つのマシン命令に分解されているせいで結果が壊れます。

Python

counter = 0 def add(): global counter for _ in range(100_000): counter += 1 # 読み出し → +1 → 書き戻し

counter += 1 の中身は下記のとおりです。

  1. RAM から counter をレジスタへ読み出す
  2. レジスタの値に 1 を足す
  3. レジスタの値を RAM に書き戻す

スレッド A が 1 まで進んだ瞬間にスレッド B も 1 を実行すると、両方とも同じ古い値を読んでしまい、足し算が 1 回分消える ことが起きます。

図解

diagram (will load when visible)

クリティカルセクション

「並行に実行されると問題が起きる区間」を クリティカルセクション と呼びます。これを 同時に 1 スレッドだけが入る 状態に保てれば、レースコンディションは消えます。これを 相互排除(mutual exclusion) と言います。

実現方法は下記の 3 つに大別されます。

方法仕組み
アトミック命令CPU 命令 1 つで完結する操作__atomic_fetch_add, compare_and_swap
ロック入室前に獲得、退室時に解放mutex、spinlock
ロックフリー / Wait-Free アルゴリズム工夫した順序操作ロックフリーキュー、RCU

具体例

ロックで保護する書き方は次のレッスンで扱います。先に アトミック整数 で解決する例を見ましょう。

c

#include <stdatomic.h> atomic_int counter = 0; void *worker(void *_) { for (int i = 0; i < 100000; i++) atomic_fetch_add(&counter, 1); return NULL; }

CPU の専用命令(lock xadd 等)で 読み・加算・書き戻し が 1 命令にまとめられるため、割り込みで分断されません。

Java では AtomicInteger、Go では sync/atomic、Rust では AtomicI32 が同じ役割を果たします。

TOCTOU バグ

「Time of Check to Time of Use」の略で、レースコンディションの代表的なセキュリティ問題です。

c

if (access(file, W_OK) == 0) { // チェック int fd = open(file, O_WRONLY); // 使用 ... }

accessopen の間に攻撃者がファイルをシンボリックリンクにすり替えると、本来書き込み権限のないファイルに書き込めてしまいます。Set-UID プログラムでは大問題です。

トレードオフ

  • ロックは確実ですが性能を落とします(特にロック競合が激しい場合)
  • アトミック操作は速いですが、扱える対象がプリミティブな型に限られます
  • ロックフリーは性能の最高峰ですが、設計が非常に難しく、メモリオーダリングを正しく扱えないと別のバグを生みます

よくある誤解

  • 「シングルコアならレースは起きない」は誤りで、コンテキストスイッチがある以上シングルコアでも起きます
  • 「Python は GIL があるから安全」も誤解で、GIL が保護するのは個々のバイトコード 1 命令だけです。+= のような「単純に見える」操作も実際には複数のバイトコード(LOAD_GLOBAL → BINARY_ADD → STORE_GLOBAL 等)に分かれるため、その間にコンテキストスイッチが入り得ます。複合操作(リスト → ソート → 検索 → 更新)はさらに多くのバイトコードにまたがるため、より一層割り込まれやすくなります

やってみよう

上記の Python サンプルを 4 スレッドで走らせ、想定値 400000 と実際値の差を観察してください。ロックなしで動かすと毎回違う値が出るのが体感できます。続いて threading.Lock で囲うと結果が安定します。

まとめ

このレッスンの要点は、競合の発生条件 / クリティカルセクション / 解決の 3 アプローチ の 3 点です。実務でこれらを切り分けて説明できるようになると、設計レビューや障害対応の精度が一段上がります。

理解度チェック

Q. 次のうち、レースコンディションが「起きない」条件はどれ?

  1. シングルコアで動作している
  2. Python の GIL が有効である
  3. 共有データを 1 スレッドだけが触る (相互排除が常に成立)
  4. メモリが十分にある
答えを見る

正解は 3。シングルコアでもコンテキストスイッチで競合は起きえます。Python の GIL があっても += のような単純操作でも複数バイトコードにまたがるため割り込まれ得ます。複合操作(リスト → ソート → 検索 → 更新)はさらに多くのバイトコードにまたがり、より一層割り込まれやすくなります。「同時に 1 スレッドしか触らない」状態 (相互排除) だけが本質的な解決策です。

出典 (References)

最終更新: 2026-05-28

関連レッスン

よくある質問

Q. このトピックは実務でどう役立ちますか?

A. DB のクエリ最適化、API 設計、データ構造の選択など、設計判断の根拠になります。表面的にライブラリを使うだけでなく「なぜそれが速いのか」を理解できると、性能問題を未然に防げます。コーディング面接でも頻出のテーマです。

Q. 計算量はどう求めれば良いですか?

A. ループのネストごとに掛け算する、再帰なら漸化式から解く、というのが基本です。Big-O 表記は定数倍と低次の項を無視するため、n の指数(n²、n log n など)に注目してください。最悪・平均・最良の 3 つを意識すると説得力が増します。

Q. 覚えるべき定番アルゴリズムは何ですか?

A. 二分探索、クイックソート/マージソート、BFS/DFS、ダイクストラ、DP の基本問題(フィボナッチ・ナップサック)が必修です。これらを「白紙から書ける」状態にすると、応用問題が一気に解けるようになります。

次のレッスン

次は Mutex と Semaphore で、レースコンディション を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. レース の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. レース とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン