核心概念#
你在最愛的餐廳用完主餐,問服務生有沒有蘋果派。他回頭看了展示櫃,看到還有一塊,說有。你點了,心滿意足。
同時,在餐廳另一邊,另一位客人問了同樣的問題。她的服務生也看了展示櫃,確認有一塊,客人也點了。
其中一位客人注定會失望。
把展示櫃換成共同銀行帳戶,服務生換成銷售終端機——你和你的伴侶同時決定買新手機,但帳戶只夠買一支。某人——銀行、商店、或你——將會非常不高興。
Tip 57 - Shared State Is Incorrect State(共享狀態就是不正確的狀態)
問題出在共享狀態。每個服務生看展示櫃時都沒有考慮另一個服務生。每個銷售終端機查看帳戶餘額時都沒有考慮另一台。
非原子性更新#
以餐廳為例,兩個服務生的程式碼像這樣:
if display_case.pie_count > 0
promise_pie_to_customer()
display_case.take_pie()
give_pie_to_customer()
end服務生 1 取得派的數量,發現是 1,向客人承諾。但此時服務生 2 也在執行,也看到數量是 1,也做了承諾。其中一個人搶到最後一塊派,另一個則進入某種錯誤狀態。
問題不在於兩個程序可以寫入同一塊記憶體,而在於沒有任何程序能保證它看到的狀態是一致的。當服務生執行 display_case.pie_count() 時,他們把值複製到自己的記憶體。如果展示櫃中的值改變了,他們用來做決定的記憶體就已經過時了。
這是因為取得值和更新值不是原子操作——底層的值可能在中間被改變。
信號量與互斥鎖#
信號量(semaphore)是一次只能有一個人持有的東西。你可以創建一個信號量來控制對某資源的存取。
餐廳用一個塑膠小矮人放在派的展示櫃上來解決問題。任何服務生在賣派之前,必須先拿到小矮人。訂單完成後再歸還。
case_semaphore.lock()
if display_case.pie_count > 0
promise_pie_to_customer()
display_case.take_pie()
give_pie_to_customer()
end
case_semaphore.unlock()假設兩個服務生同時執行:他們都嘗試鎖定信號量,但只有一個成功。拿到的繼續正常執行,沒拿到的被暫停等待。第一個完成後解鎖,第二個繼續,發現沒有派了,向客人道歉。
信號量的問題: 這個方法只在所有人都遵守使用信號量的約定時才有效。如果某個開發者忘了(或不知道要用),就又回到混亂狀態。
讓資源成為交易性的#
更好的設計是將保護責任移到資源本身,而不是委託給使用者。改變 API 讓服務生可以在一次呼叫中檢查數量並取走派:
slice = display_case.get_pie_if_available()
if slice
give_pie_to_customer()
end但即使集中化了,方法本身仍可能被多個並行執行緒呼叫,所以內部仍需要信號量保護。更要注意的是,如果信號量鎖定後的程式碼拋出異常,信號量可能永遠不會解鎖,導致後續所有存取都掛起。因此需要 try/ensure 來保證解鎖。許多語言提供了處理這種情況的函式庫(如 protect 區塊)。
多資源交易#
餐廳新增了冰淇淋。如果客人點「派配冰淇淋」,服務生需要同時確認派和冰淇淋都有。
如果分開取得——先拿到派,再去拿冰淇淋但發現沒了——你手上就拿著一塊派卻無法完成訂單,而且派因為被你拿走了,其他客人也拿不到。
這段程式碼會變得非常醜陋,充滿巢狀的 try/rescue 區塊。作者認為務實的做法是將「派配冰淇淋」視為自己的資源,建立一個新模組來處理,甚至建立某種菜單項目,包含元件參照和一個通用的 get_menu_item 方法來做資源的取得與歸還。
非交易性更新#
共享狀態問題不只出現在記憶體中,而是出現在任何應用程式共享可變資源的地方:檔案、資料庫、外部服務等。只要兩個或更多程式碼實例可以同時存取某資源,就是潛在的問題。
實例: 作者在寫這本書的第二版時,將工具鏈改為使用多執行緒平行處理。結果建構開始在奇怪的地方隨機失敗。追蹤發現是某些程式碼會暫時改變當前目錄——在非平行版本中沒問題,但在平行版本中,一個執行緒改了目錄,另一個就找不到檔案了。因為當前目錄是執行緒間共享的。
Tip 58 - Random Failures Are Often Concurrency Issues(隨機失敗通常是並行問題)
其他排他性存取方式#
大多數語言都有某種排他性存取共享資源的函式庫支援——mutex、monitor、semaphore,都是以函式庫形式實作。
但有些語言將並行支援內建於語言本身。例如 Rust 強制「資料擁有權」概念:任何時候只有一個變數或參數能持有某個可變資料的參照。
函數式語言傾向讓所有資料不可變,使並行更簡單。但最終它們仍然必須面對真實的、可變的世界。
醫生,這樣好痛…#
如果你從這個章節只帶走一件事,請記住:在共享資源環境中的並行是困難的,自己管理它充滿了挑戰。
作者推薦那個老笑話的結論:
醫生,我這樣做會痛。
那就不要那樣做。
接下來的章節提出了不需要承受痛苦就能享受並行好處的替代方案。
相關章節#
- Topic 10,正交性
- Topic 28,去耦合
- Topic 38,巧合式程式設計