コンピューターサイエンス:アルゴリズム / OS / ネットワーク / DB
レースコンディション
レースコンディション
このレッスンで分かること
- レースコンディションは「複数のスレッド/プロセスが 同じ資源を同時に変更 し、実行順次第で結果が変わるバグ」です
- シングルスレッドでは決して起きず、並行性を導入した瞬間に頭をもたげます
- 解決には アトミック操作 か ロック(mutex、セマフォ等) が必要です
レースコンディション とは
レースコンディション。本レッスンでは、レースコンディション の基本から実際の使いどころまでを整理し、現場で迷わず使える形に落とし込みます。
レースコンディションを防ぐ 3 つのアプローチ
| アプローチ | 仕組み | 速度 | 適用範囲 |
|---|---|---|---|
| アトミック命令 | CPU 命令 1 つで読み・変更・書き戻し完結 | ◎ | プリミティブ型のみ |
| ロック (mutex / spinlock) | 入室前に獲得、退室時に解放 | ○ | 任意の処理 |
| ロックフリー / Wait-Free | CAS 等で順序を工夫 | ◎ | 設計が難しい、メモリオーダリング注意 |
代表的なバグパターンとして 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 の中身は下記のとおりです。
- RAM から
counterをレジスタへ読み出す - レジスタの値に 1 を足す
- レジスタの値を RAM に書き戻す
スレッド A が 1 まで進んだ瞬間にスレッド B も 1 を実行すると、両方とも同じ古い値を読んでしまい、足し算が 1 回分消える ことが起きます。
図解
クリティカルセクション
「並行に実行されると問題が起きる区間」を クリティカルセクション と呼びます。これを 同時に 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); // 使用
...
}access と open の間に攻撃者がファイルをシンボリックリンクにすり替えると、本来書き込み権限のないファイルに書き込めてしまいます。Set-UID プログラムでは大問題です。
トレードオフ
- ロックは確実ですが性能を落とします(特にロック競合が激しい場合)
- アトミック操作は速いですが、扱える対象がプリミティブな型に限られます
- ロックフリーは性能の最高峰ですが、設計が非常に難しく、メモリオーダリングを正しく扱えないと別のバグを生みます
よくある誤解
- 「シングルコアならレースは起きない」は誤りで、コンテキストスイッチがある以上シングルコアでも起きます
- 「Python は GIL があるから安全」も誤解で、GIL が保護するのは個々のバイトコード 1 命令だけです。
+=のような「単純に見える」操作も実際には複数のバイトコード(LOAD_GLOBAL → BINARY_ADD → STORE_GLOBAL 等)に分かれるため、その間にコンテキストスイッチが入り得ます。複合操作(リスト → ソート → 検索 → 更新)はさらに多くのバイトコードにまたがるため、より一層割り込まれやすくなります
やってみよう
上記の Python サンプルを 4 スレッドで走らせ、想定値 400000 と実際値の差を観察してください。ロックなしで動かすと毎回違う値が出るのが体感できます。続いて threading.Lock で囲うと結果が安定します。
まとめ
このレッスンの要点は、競合の発生条件 / クリティカルセクション / 解決の 3 アプローチ の 3 点です。実務でこれらを切り分けて説明できるようになると、設計レビューや障害対応の精度が一段上がります。
理解度チェック
Q. 次のうち、レースコンディションが「起きない」条件はどれ?
- シングルコアで動作している
- Python の GIL が有効である
- 共有データを 1 スレッドだけが触る (相互排除が常に成立)
- メモリが十分にある
答えを見る
正解は 3。シングルコアでもコンテキストスイッチで競合は起きえます。Python の GIL があっても += のような単純操作でも複数バイトコードにまたがるため割り込まれ得ます。複合操作(リスト → ソート → 検索 → 更新)はさらに多くのバイトコードにまたがり、より一層割り込まれやすくなります。「同時に 1 スレッドしか触らない」状態 (相互排除) だけが本質的な解決策です。
出典 (References)
- POSIX Threads — pthread_mutex_lock
- C11 standard — atomic operations (N1570)
- Linux man page — TOCTOU race conditions
最終更新: 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 つができることを確認しましょう。
- レース の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. レース とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン