テクノロジー · 実用ガイド

Python 3.12でcached_propertyが2回走る?正しさに必要な場合だけインスタンス単位で直す

Python 3.12はcached_propertyの旧ロックを削除した。重複実行が有害か確認し、必要なら成功値をロック内で保存する。

並行アクセスと冪等性から通常のcached_propertyかインスタンス単位のsingle-flightを選ぶ図。
独自の解説用概念図:重複が正しさや許容不能なコストに影響する場合だけ同期する。 Watch at Ease

キャッシュが壊れないことと、getterが一度だけ実行されることは別である。Python 3.12以降では、同じインスタンスの初回アクセスが競合すると、値が保存される前に二つのスレッドがgetterへ入れる。

反射的にロックを追加しない。変更不能な値から得る純粋で冪等な計算なら重複は主にコストの問題である。課金、ファイル作成、外部ID、共有状態の変更やオブジェクト同一性が関わるなら、単一実行を独立した要件にする。

1. 設計変更前に競合を再現する

バリアで二つのスレッドを同期し、同じインスタンスを初めて読む。getter冒頭で回数を増やし少し待つ。Python 3.12以降では、最終値が同じでも回数2は許容動作である。

Pythonの版、同一インスタンスか、フレームワーク独自デコレータかを記録する。二つのインスタンスを作る試験は別の問題を測る。

2. 時間ではなく結果で分類する

不変なフィールドの純計算は通常冪等で、二重計算は時間だけを失う。高価な計算の合流は性能判断である。

挿入、決済、ファイル作成、外部要求、カウンタ、登録は正しさに影響する。可能なら冪等キーやトランザクションも使う。プロセス内ロックは分散した厳密一回を保証しない。

3. インスタンスロック内で保存する

通常のproperty、専用番兵、キャッシュ欄、インスタンスロックを使う。高速経路は既存値を返し、低速経路はロック後に再確認し、計算・保存してから解放する。

cached_propertyのgetter内だけをロックし、デコレータが解放後に値を保存する形では小さな競合窓が残り得る。

4. 失敗と無効化の意味を守る

計算成功後だけ代入する。例外なら番兵が残り次回に再試行できる。未完成の可変オブジェクトを先に公開しない。

無効化も同じロック内で行い、進行中の読者が旧値を使えるか決める。Noneが正当な結果なら番兵に使わない。

5. Python 3.11の競合を再現しない

デスクリプタやクラスのロックは無関係なインスタンスを直列化し、旧設計の問題を戻す。アクセスごとに新しいロックを作っても相互排他にならない。

オブジェクトごとのロックが重いなら、事前計算、不変化、要求を合流できる外部キャッシュ、副作用の再設計を検討する。

6. 四つの試験で受け入れる

同一インスタンスの二並行読みが一回だけ計算、別インスタンスは重なって実行、初回例外後は再試行、ロック内無効化後は新値一つだけを確認する。

対応する全Python版で実行し、未公開の昔の実装ではなく自分の並行契約を検証する。

番兵確認、ロック、再確認、計算、解放前保存というインスタンス単位の流れ。
独自の解説用概念図:再確認とロック内保存で重複初期化を閉じる。 Watch at Ease