所謂狀態(tài),就是在某個時間點上一個標識所代表的值。
Clojure 的引用模型把標識和值清晰地區(qū)分開來。在 Clojure 中,幾乎所有的東西都是值。為了加以標識,Clojure提供了四種引用類型。
- 引用(Ref),負責(zé)協(xié)同地、同步地更改共享狀態(tài)。
- 原子(Atom),負責(zé)非協(xié)同地、同步地更改共享狀態(tài)。
- 代理(Agent),負責(zé)異步地更改共享狀態(tài)。
- 變量(Var),負責(zé)線程內(nèi)的狀態(tài)。
應(yīng)用與軟事務(wù)內(nèi)存
Clojure 中的大多數(shù)對象都是不可變的。當你真的想要可變數(shù)據(jù)時,你必須明確地表示出來。比如說,你可以像下面這樣創(chuàng)建一個可變的引用(ref),讓它指向不可變對象。
(ref initial-state)
寫個例子:
user=> (def s (ref "hahaha"))
#'user/s
引用包裝并保護了對其內(nèi)部狀態(tài)的訪問。要讀取引用的內(nèi)容,你可以調(diào)用deref。
(deref reference)
deref函數(shù)可以縮寫為讀取器宏@。
user=> (deref s)
"hahaha"
user=> @s
"hahaha"
ref-set
你可以使用ref-set來改變一個引用所指向的位置。
(ref-set reference new-value)
因為引用是可變的,你必須在更新它們時施以保護。在Clojure中,你可以使用事務(wù)。事務(wù)被包裹在dosync之中。
(dosync& exprs)
修改前面的引用。
user=> (dosync (ref-set s "heihei"))
"heihei"
事務(wù)的屬性
和數(shù)據(jù)庫事務(wù)一樣,STM事務(wù)也具有一些重要的性質(zhì)。
- 更新是原子的(atomic)。如果你在一個事務(wù)中更新了多個引用,所有這些更新的累積效果,在事務(wù)外部看來,就好像是在一個瞬間發(fā)生的。
- 更新是一致的(consistent)。可以為引用指定驗證函數(shù)。如果這些函數(shù)中的任何一個失敗了,整個事務(wù)都將失敗。
- 更新是隔離的(isolated)。運行中的事務(wù),無法看到來自于其他事務(wù)的局部完成結(jié)果。
alter
Clojure的alter能在事務(wù)中對引用對象應(yīng)用一個更新函數(shù)。
(alter ref update-fn & args...)
alter會返回這個引用在事務(wù)中的新值。當事務(wù)成功完成后,引用將獲得它在事務(wù)中的最后一個值。用alter來替代ref-set能使代碼更具可讀性。
看個簡單的例子。
(def messages (ref ()))
(defn add-message [msg]
(dosync (alter messages conj msg)))
(add-message "abc")
(add-message "123")
(println @messages)
輸出如下:
(123 abc)
注意這里的更新函數(shù)用了conj,alter函數(shù)調(diào)用它的update-fn時,把當前引用的值作為其第一個參數(shù),這正是conj所期望的。
STM工作原理:MVCC
Clojure 的 STM 采用了一種名為多版本并發(fā)控制(Multiversion Concurrency Control,MVCC)的技術(shù),這種技術(shù)也被用在了幾個主要的數(shù)據(jù)庫中。
下面說明了在Clojure中,MVCC是如何運作的。
事務(wù) A 啟動時會獲取一個“起始點”,這個起始點就是一個簡單的數(shù)字,被當作STM世界中的唯一時間戳。在事務(wù)A中訪問任何一個引用,實際上訪問的是這個引用與起始點相關(guān)的一份高效副本。Clojure 的持久性數(shù)據(jù)結(jié)構(gòu)使得提供這些高效的私有副本相當廉價。
在事務(wù)A中,對引用進行操作時依賴(以及返回)的這個私有副本的值,被稱為事務(wù)內(nèi)的值。在任意時間點,如果STM檢測到其他事務(wù)設(shè)置或更改了某個引用,而事務(wù)A正好也想要設(shè)置或更改,那么事務(wù)A將被迫重來。如果你在dosync塊中拋出了一個異常,那么事務(wù)A會終止,而非重試。
事務(wù)A一旦提交,它一直以來那些私有的寫操作就會暴露給外部世界,而且是在這個事務(wù)時間軸的一個點上瞬間發(fā)生的。
commute
commute是一種特殊的alter變體,允許更多并發(fā)。
(commute ref update-fn & args...)
當然,這需要進行權(quán)衡。之所以名為 commute ,是因為它們必須是可交換的commutative)。也就是說,更新操作必須能以任何的次序出現(xiàn)。這就賦予了 STM 系統(tǒng)對commute重新排序的自由。
使用原子進行非協(xié)同、同步的更新
相比引用,原子是一種更加輕量級的機制。在事務(wù)中對多個引用進行更新會被協(xié)同,而原子則允許更新單個的值,不與其他的任何事物協(xié)同。
你可以使用atom來創(chuàng)建原子,它的函數(shù)簽名與ref非常類似。
(atom initial-state options?)
; options包括:
; :validator一個驗證函數(shù)
; :meta一個元數(shù)據(jù)映射表
創(chuàng)建一個原子。
user=> (def s (atom "haha"))
#'user/s
對原子解引用就可以得到它的值,這和引用是一樣的。
user=> @s
"haha"
原子并不參與事務(wù),因而不需要dosync。要為一個原子設(shè)置值,簡單的調(diào)用reset!即可。
(reset! an-atom newval)
修改上面的原子。
user=> (reset! s "ddd")
"ddd"
使用代理進行異步更新
有的應(yīng)用程序會有這樣一些任務(wù),任務(wù)之間只需要很少地協(xié)同就能彼此獨立進行。Clojure提供了代理來支持這種風(fēng)格的任務(wù)。
代理和引用有很多共同點。和引用一樣,你可以通過包裝初始狀態(tài)來創(chuàng)建代理。
(agent initial-state)
下面創(chuàng)建了一個計數(shù)器的代理,并把初始計數(shù)值設(shè)置為0。
user=> (def counter (agent 0))
#'user/counter
一旦得到了一個代理,你就可以向它send一個函數(shù),來更新其狀態(tài)。send把函數(shù)update-fn放進線程池里的某個線程中開始排隊,等待隨后執(zhí)行。
(send agent update-fn & args)
向代理進行發(fā)送,和對引用進行交換非常相像。下面告訴計數(shù)器counter,準備好要自增(inc)了。
user=> (send counter inc)
#object[clojure.lang.Agent 0x4a11eb84 {:status :ready, :val 1}]
調(diào)用send不會返回代理的新值,而是返回了代理本身。
就像引用一樣,你可以用deref或是@來檢查代理當前的值。
user=> @counter
1
如果你希望確保代理已經(jīng)完成了你發(fā)送給他的動作,你可以調(diào)用await或者await-for。
(await & agents)
(await-for timeout-millis & agents)
這兩個函數(shù)會導(dǎo)致當前線程阻塞,直到所有發(fā)自當前線程或代理的動作全部完成。如果超過了超時時間,await-for會返回空,否則會返回一個非空值。await沒有超時時間,所以一定要小心:await愿意永遠等下去。
統(tǒng)一的更新模型
引用、原子和代理都提供了基于它們當前的狀態(tài),通過應(yīng)用其他函數(shù)來更新這些狀態(tài)的函數(shù)。
| 更新機制 | 引用函數(shù) | 原子函數(shù) | 代理函數(shù) |
|---|---|---|---|
| 應(yīng)用函數(shù) | alter | swap! | send-off |
| 函數(shù)(交換) | commute | 不適用 | 不適用 |
| 函數(shù)(非阻塞) | 不適用 | 不適用 | send |
| 簡單設(shè)置 | ref-set | reset! | 不適用 |
用變量管理線程內(nèi)狀態(tài)
大多數(shù)變量都甘愿保持它們的根綁定永不改變。然而,你可以借助binding宏,為一個變量創(chuàng)建線程內(nèi)的綁定。
(binding [bindings] & body)
綁定具有動態(tài)范圍。換句話說,在binding創(chuàng)建的這個范圍內(nèi),線程執(zhí)行過程中需要經(jīng)過的任何地方,綁定都是可見的,直到該線程離開了該范圍。同時對于其他線程而言,綁定也是不可見的。
首先需要聲明一個動態(tài)變量。
user=> (def ^:dynamic foo 10)
#'user/foo
在結(jié)構(gòu)上,binding與let看起來非常相像。下面為foo創(chuàng)建一個線程內(nèi)綁定,并檢查它的值。
user=> (binding [foo 2] foo)
2