Engineering · 1 min read
雙重檢查鎖定:兩個看似相同的檢查各司其職
雙重檢查鎖定寫出兩個看似重複的 if 判斷,其實各自解決不同問題。拿掉任何一個,惰性初始化都會在併發下悄悄壞掉。
雙重檢查鎖定:兩個看似相同的檢查,其實各司其職
若想用最小的鎖成本惰性初始化共享狀態,就會寫出兩個一模一樣的 if。它們看起來重複,其實解決不同問題——拿掉任何一個都會壞。
- 檢查 1(不加鎖): 熱路徑的快速出口。若值已快取且新鮮,直接回傳、完全不取鎖 → 並發讀取者不需排隊。
- 檢查 2(持鎖中): 涵蓋「#1 失敗」到「取得鎖」之間的空檔。這段時間另一條執行緒可能已經建好了值——再檢查一次才不會重 build。
def login(self):
if self._fresh(): # 檢查 1:不加鎖的快速出口
return self.session
with self._login_lock:
if self._fresh(): # 檢查 2:別人可能已經建好了
return self.session
self._do_login() # 真正重建
return self.session
可以拿掉哪個?
- 拿掉 #1 →「單重檢查鎖定」:仍正確,只是每次呼叫都取鎖。
- 拿掉 #2 → 去重失效:兩個 miss 的執行緒都會重建並互相覆蓋。
什麼時候才值得? 只有當熱讀取路徑有競爭、且臨界區成本不小時,DCL 才划算。若每次呼叫本來就被 DB 查詢(~ms)主導,鎖成本(~ns)只是雜訊——這時 DCL 只是慣例寫法,而非效能關鍵,單重檢查更簡單。
重複的程式碼不一定是壞味道——先問它們是不是在解不同的問題。
參考來源:
- https://en.wikipedia.org/wiki/Double-checked_locking
- https://java-design-patterns.com/patterns/double-checked-locking/
Related: 參見with lock: 如何保證例外時也釋放鎖,或回到讓 login 執行緒安全的四筆學費總覽。