Race Condition とは?タイミングが結果を変えるとき
race condition は、プログラムの結果がロジックだけでなく、並行動作の予測不能な順序やタイミングに依存するバグです。
2 つ以上の thread やプロセスが同時に共有状態に触れると、それらが動く順序によって結果が変わりうります。プログラムが 1 つの順序を仮定しているのに別の順序が起こりうるなら、それが race condition です。race は間欠的であるため悪名高く、同じコードが 1000 回 pass して 1001 回目に fail することがあります。
race がどう起こるか
2 つの thread がどちらもカウンタを読み、1 を足し、書き戻すと想像してください。ちょうど悪い具合に交錯すると、両方が同じ開始値を読み、片方の更新が失われます。バグはどちらの thread 単独にもありません。共有状態への同期されていないアクセスにあります。
なぜ race はこれほど難しいのか
- タイミングに依存するため、予測不能に現れては消えます。
- しばしば負荷がかかったとき、あるいはより速い/遅いマシンでのみ現れます。
- ローカルで pass して CI で fail することも、その逆もあります。
- 確実に再現するのが難しく、デバッグを遅らせます。
race を防ぐ方法
共有状態へのアクセスを lock、アトミック操作、あるいはキューや不変データのような高レベルの仕組みで調整してください。最も安全な修正は多くの場合、可変状態の共有を完全にやめ、その場で変更する代わりにデータを渡すことです。
race と deadlock
race condition は調整が足りなさすぎることです。thread が互いを踏みつけます。deadlock は調整が多すぎることです。thread が互いを永遠に待ちます。どちらも並行性から来ますが、正反対の失敗モードです。
簡単な例
同じ一時ファイルに分離なしで書き込む 2 つのテストは競合しえます。スケジューリング次第で一方が他方を上書きし、どちらが fail するかは実行ごとに変わります。
CI における race condition
race は flaky なテストの主要な原因です。suite は速いラップトップでは pass しますが、タイミングがずれる混雑した CI runner では間欠的に fail します。修正は race を取り除くことであり、retry ではありません。とはいえ Latchkey は本当の原因を切り分ける間、実行を自動 retry できるため、1 回の flaky な失敗が merge をブロックしません。
重要なポイント
- race condition は結果を、予測不能な並行のタイミングに依存させます。
- race は共有された可変状態への同期されていないアクセスから生じます。
- CI の flaky なテストの主要な原因であり、retry するだけでなく修正しなければなりません。