Clojure軟體事務儲存器
多核或多CPU使得併發的要求更加迫切,傳統使用鎖來管理併發,遺憾的是已被證明不太理想,因為它們經常導致死鎖、飢餓、競爭和容易出錯。在這篇文章中,我們將探討如何利用Clojure的軟體事務儲存器(STM)來更好地利用現代硬體。
Clojure的方式
併發難題的核心是狀態問題,實質上,大多數程式語言都會使用多執行緒共享資料更新的方式管理狀態。
Clojure提供不可變資料結構和語言級語義來解決這個狀態的併發更新問題,通過對狀態的託管引用實現安全併發,這是由稱為軟體事務記憶體(STM)的控制機制提供的。
STM是一種軟體級設計,用於控制對記憶體中共享資料的訪問,它的工作方式類似於資料庫事務(但限於控制對記憶體資料的訪問,而不是持久的資料儲存)。
STM事務確保以原子方式完成狀態更改 ,與事務關聯的所有更改,一旦過程中有失敗,則全部回滾已經做出的更改,此外,必須實現一致地進行更改,這意味著如果更改無法滿足某些指定的約束,則事務也會失敗,最後,更改必須以隔離的方式發生,對事務中的資料所做的所有更改僅對當前的執行緒是可見的。
Clojure STM使用多版本控制併發,它在事務開始之前獲取資料的快照,對快照所做的更改對於外部世界(其他執行緒)是不可見的,直到提交更改成功完成事務。
Clojure提供了ref(reference)結構,用於管理允許同步和
協調更改的資料,對refs的所有修改必須在STM事務中發生,該事務是使用dosync語法指定的。
銀行業務情景
讓我們考慮典型的銀行賬戶處理方案,在一個帳戶甚至多個帳戶上進行大量交易。但是,該帳戶預計必須在所有這些交易中保持一致的平衡。
讓我們建立一個函式,使用ref建立一個帳戶,指定帳戶名、帳號和初始起始餘額。
(defn create-account [ account-name account-number balance ]
( ref { :account-name account-name
:account-number account-number
:balance balance
} :validator allowable-balance? ) )
(defn allowable-balance? [ { :keys [ balance ]}]
( or ( > balance 0 )
(throw (IllegalStateException. "Balance cannot be less than zero" ) )
))
此函式返回一個ref,它表示呼叫時的帳戶。我們還添加了
驗證器功能,以確保帳戶餘額始終大於零。(畢竟我們是銀行。)
現在,讓我們建立三個帳號。
(def first-account ( create-account "Robert" 300045 120 ) )
( def second-account (create-account "Mike" 30046 500 ) )
( def third-account (create-account "Rose" 30047 200 ) )
這將分別建立具有指定詳細資訊的三個帳戶引用,現在,假設Mike決定做慈善事業並指示銀行從他的賬戶轉賬到Robert賬戶200美元。
要做到這一點,必須做兩件事:首先,我們需要將Mike的賬戶扣除200美元,然後將其存入Robert的賬戶。對於外界來說,這兩個操作必須同時發生(原子),轉移需要以這種一種方式進行:即從任何外部觀察者的角度來看,被轉移的資金一次只能在一個賬戶中。
為此,Clojure要求使用dosync在事務中實現此操作。
讓我們定義一個名為make-transfer的函式,它將指定數量的資金從一個帳戶轉移到另一個帳戶。
(defn make-transfer [ from-account to-account transfer-amount ] (dosync (alter from-account update-in [ :balance ] - transfer-amount ) (alter to-account update-in [ :balance ] + transfer-amount )))
現在,讓我們發出轉移:
(transfer second-account first-account 200 )
(println "first account -> " @first-account "Second account -> " @second-account )
這是我螢幕上顯示的輸出。
first account -> {:account-name Robert, :account-number 300045, :balance 320} Second account -> {:account-name Mike, :account-number 30046, :balance 300}
但是,下面這樣的轉移失敗了:
first account -> {:account-name Robert, :account-number 300045, :balance 320} Second account -> {:account-name Mike, :account-number 30046, :balance 300}
出現錯誤:
CompilerException java.lang.IllegalStateException: Balance cannot be less than zero, compiling:(~/clojure/concurrency.clj:44:1)
我們可以在不同的執行緒上執行這兩個操作,如下所示:
(future (make-transfer second-account first-account 100 ) ) (future (make-transfer second-account third-account 600) )
理解alter函式
alter函式用於原子地更新一個Ref物件,它由提供的ref和一個函式來呼叫,該函式將ref作為引數並返回一個值,返回值將用作ref的新值。因為Clojure允許在多個執行緒中同時執行多個事務,所以傳給alter函式的ref的快照值如果與ref的當前值相同時才允許事務提交,否則所有更改都將是丟棄,並使用ref的最新值重試事務。這種無鎖方法允許執行緒自由執行事務而不會被阻塞,包括那些只能讀取的事務。
commute函式
為了限制alter function在提交時需要的重試次數,可以在函式應用程式的順序無關緊要的情況下使用commute函式,如果在兩個事務結束時,哪個執行緒是否首先提交本身無所謂,可以採用這個函式。
因此,銀行轉賬的順序並不重要的情況下,可以像下面這樣實施轉移功能:
(defn make-transfer-2 [ from-account to-account transfer-amount ] (dosync (commute from-account update-in [ :balance ] - transfer-amount ) (commute to-account update-in [ :balance ] + transfer-amount ) ))
結論
Clojure是一種動態型別的函式式語言,它的設計考慮了併發性。