科技 · 實用指南
Python 3.12 的 cached_property 會執行兩次?只在正確性需要時加單航班鎖
Python 3.12移除了cached_property舊有的描述器級鎖。先證明重複計算有害,再以每個實例的鎖只發布成功結果。

快取最後保持一致,不代表getter只執行一次。Python文件中的執行緒安全主要指快取結構不會損壞;自Python 3.12起,同一實例首次存取時,兩條執行緒都可能在結果寫入前進入getter。
不要反射式地加鎖。純粹且冪等的派生計算重複一次通常只是效能成本;若getter會扣款、建立檔案、配置外部ID、修改共享狀態或依賴物件身分,就要把單次執行視為獨立的正確性要求。
1. 改設計前先證明競爭
用屏障讓兩條執行緒同時首次讀取同一實例。getter開始時增加計數並短暫停頓;在Python 3.12以上,即使最後讀到相同快取值,計數為2仍符合文件。
記錄Python版本、兩條執行緒是否共用同一實例,以及框架是否替換了cached_property。建立兩個實例的測試驗證的是另一個問題。
2. 依後果而非僅依成本分類
只讀不可變欄位的純計算通常冪等,重複執行只是浪費時間。昂貴工作可因效能而合併,但那不是同一項正確性判斷。
資料庫插入、付款、建立檔案、外部請求、計數器與一次性註冊會影響正確性。能用冪等鍵或交易時優先採用;程序內鎖不能保證跨程序或重試的全域恰好一次。
3. 在每實例鎖內儲存結果
使用一般property、私有哨兵、快取欄位與實例鎖。快速路徑直接回傳;慢速路徑加鎖後再檢查,計算並寫入才釋放。
順序必須是檢查、加鎖、再檢查、計算、指派、釋放。若只在cached_property getter中加鎖而由裝飾器於鎖外寫值,仍可能留下競爭窗口。
4. 保留失敗與失效語意
只有計算成功後才指派。例外會保留哨兵,下一次存取可以重試;不要先公開半初始化物件。
若允許清除快取,應在同一實例鎖內執行,並說明進行中的讀者能否繼續用舊物件。None若是合法結果,就不可拿來當哨兵。
5. 不要重建Python 3.11爭用
把鎖放在描述器或類別上會序列化不相干實例,重現舊設計被移除的原因。每次存取新建鎖也不會互斥。
若每實例鎖成本不可接受,可考慮預先建構、不可變預計算、支援請求合併的外部快取,或重新設計副作用。
6. 用四項測試驗收
測試同一實例兩條並發讀取只計算一次;兩個實例可重疊;首次拋錯後能重試;鎖內失效後只發布一個新值。
在所有支援的Python版本上執行。目標是明確應用自己的並發契約,不是模仿未公開的歷史實作。
