レース条件は「同時に触ると壊れる」バグ
レース条件(race condition)は、複数の処理が同時に動いたときに、どちらが先に進んだかによって結果が変わってしまう状態のことです。英語の race はそのまま「競争」で、複数の処理が同じものを取り合って競走している、というイメージから来ています。
やっかいなのは、いつも壊れるわけではないところ。10回動かして9回は正常、1回だけおかしい、みたいな出方をします。「さっき再現したのにもう一回やったら直った」みたいなバグの多くは、この手のものです。
身近な例:冷蔵庫の最後のプリン
同居している2人が、それぞれ冷蔵庫を開けて「プリンが1個ある」と確認したとします。2人とも「じゃあ自分が食べよう」と思う。ここまでは、どちらも間違ったことはしていません。
ところが実際に手を伸ばすと、プリンは1個しかない。先に取った方が食べて、後から来た方は空っぽの冷蔵庫を見ることになります。「確認した瞬間」と「実際に取る瞬間」のあいだに、もう一方が割り込んでいるわけです。
プログラムでも同じことが起きます。「残り在庫を確認する」「在庫を1つ減らす」という2ステップの処理を、2つのリクエストが同時に走らせると、両方が「在庫1つあり」を読んでしまい、結果として在庫がマイナスになる。これが典型的なレース条件です。
なぜ「たまにしか」起きないのか
2つの処理がぴったり噛み合うタイミングは、そう頻繁には訪れません。処理のわずかな速度差、ネットワークの遅延、マシンの負荷などで、たまたま重なったときだけ問題が表面化します。
だから開発中のノートPCでは一度も出ないのに、本番でアクセスが増えた途端に発生する、という展開になりがちです。ログを見ても、それぞれの処理単体は正常に見えるので原因追跡も難しい。「再現しないバグ」の代表格と言われるのはこのためです。
起きやすい場面
- 同じファイルを2つの処理が同時に書き換える
- 同じデータベースの行を、読んでから書き戻すまでに別の処理が触る
- 複数の処理が同じカウンタや共有の変数を増減させる
- 「ファイルがあるか確認してから作る」のように、確認と実行が分かれている処理
共通しているのは、「読む」と「書く」のあいだに隙間があること。この隙間に他人が入り込めるかどうかが分かれ目です。
基本的な対処の考え方
王道は「同時に触らせない」ことです。トイレの個室のように、誰かが入っている間は鍵をかけて他の人を待たせる。プログラムではこれをロックと呼びます。
もうひとつは「確認と実行を分けない」やり方。冷蔵庫の例なら、覗いてから取るのではなく、手を突っ込んで掴めたら自分のもの、という一動作にしてしまう。データベースでいえば、読み取りと更新をひとつの命令やトランザクションにまとめる発想です。
そもそも共有をやめる、という選択肢もあります。1個のプリンを2人で取り合うからもめるので、最初からそれぞれ専用のものを用意しておけば競争は起きません。処理ごとに別のファイルや別の作業ディレクトリを使う、といった設計がこれにあたります。
AIにコードを書かせるときにも関係する
複数の作業を並行して走らせるような使い方をすると、それぞれが同じファイルを書き換えて内容が食い違う、といった事態が起こり得ます。並行処理を扱うときは「このファイルは誰が触るのか」を意識しておくと、後から原因不明の不整合に悩まされずに済みます。
レース条件は、書いたコード自体は一行ずつ正しいのに、組み合わせと順序のせいで壊れる種類のバグです。「単体では正しい」ことと「同時に動かしても正しい」ことは別物だ、と覚えておくと見つけやすくなります。
このページは役に立ちましたか?