### Refs
Refs是用來協調對于一個或者多個binding的并發修改的。這個協調機制是利用 [Software Transactional Memory](http://en.wikipedia.org/wiki/Software_transactional_memory) (STM)來實現的。 Refs指定在一個事務里面修改。
STM在某些方面跟數據庫的事務很像。在一個STM事務里面做的修改只有在事務提交之后別的線程才能看到。這實現了ACID里面的A和I。Validation函數是的對Ref的修改與跟它相關的其它的值是一致的(consistent), 也就實現了C。
要想你的代碼在一個事務里面執行, 那么要把你的代碼包在宏 `dosync` 的體內。當在一個事務里面對值進行修改,被改的其實是一個私有的、線程內的、直到事務提交才會被別的線程看到的一快內存。
如果到事務結束的時候也沒有異常拋出的話, 那么這個事務會順利的提交, 在事務里面所作的改變也就可以被別的線程看到了。
如果在事務里面有一個異常拋出,包括validation函數拋出的異常,那么這個事務會被回滾,事務里面對值做的修改也就會撤銷。
如果在一個事務里面,我們要對一個Ref進行修改,但是發現從我們的事務開始之后,已經有別的線程對這個Ref做了改動(沖突了), 那么當前事務里面的改動會被撤銷,然后從dosync的開頭重試。那到底什么時候會檢測到沖突, 什么時候會進行重試, 這個是沒有保證的, 唯一保證的是clojure為檢測到沖突,并且會進行重試。
要在事務里面執行的代碼一定要是沒有副作用的,這一點非常重要,因為前面提到的,事務可能會跟別的事務事務沖突,然后重試, 如果有副作用的話,那么出來的結果就不對了。不過要執行有副作用的代碼也是可能的, 可以把這個方法調用包裝給Agent, 然后這個方法會被hold住直到事務成功提交,然后執行一次。如果事務失敗那么就不會執行。
`ref` 函數可以創建一個 Ref 對象。下面的例子代碼創建一個Ref并且得到它的引用。
```
(def <em>name</em> (ref <em>value</em>))
```
`dosync` 宏用來包裹一個事務 -- 從它對應的左括號開始,到它對應的右括號結束。在事務里面我們用 `ref-set` 來改變一個Ref的值并且返回這個值。你不能在事務之外調用這個函數,否則會拋出 `IllegalStateException` 異常。 看例子:
```
(dosync
...
(ref-set <em>name</em> <em>new-value</em>)
...)
```
如果你要賦的新值是基于舊的值的話,那么就需要三個步驟了:
1. deference 這個 Ref 來獲得它的舊值
2. 計算新值
3. 設置新值
`alter` 和 `commute` 函數在一個操作里面完成這三個步驟。 `alter` 函數是用來操作那些必須以特定順序進行的修改。而 `commute` 函數則是要來操作那些修改順序不是很重要 -- 可以同時進行的修改。 跟 `ref-set` , 一樣, 它們只能在一個事務里面調用。它們都接受一個 "update 函數" 做為參數, 以及一些額外的參數來計算新的值。這個函數會被傳遞這個Ref在線程內的當前的值以及一些額外的參數(如果有的話)。當我們要賦的新的值是基于舊的值計算出來的時候, 那么我們鼓勵使用 `alter` 和 `commute` 而不是 `ref-set` .
比如,我們想給一個Ref: `counter` 加一, 我們可以用 `inc` 函數來實現:
```
(dosync
...
(alter counter inc)
; or as
(commute counter inc)
...)
```
如果 `alter` 試圖修改的 Ref 在當前事務開始之后被別的事務改變了,那么當前事務會進行重試。而同樣的情況下 `commute` 不會進行重試。它會以事務內的當前值進行計算。這會獲得比較好的性能(因為不進行重試)。但是要記住的是 `commute` 函數只有在多個線程對Ref的修改順序不重要的時候才能使用。
如果一個事務提交了, 那么對于 `commute` 函數還會有一些額外的事情發生。對于每一個 `commute` 調用, Ref 的值會被下面的調用結果重置:
```
(apply <em>update-function</em> <em>last-committed-value-of-ref</em> <em>args</em>)
```
注意,這個update-function會被傳遞這個Ref最后被提交的值, 這個值可能是另外一個、在我們當前事務開始之后才開始的事務。
使用 `commute` 而不是 `alter` 是一種優化。只要對Ref進行更新的順序不會影響到這個Ref的最終的值。
然后看一個使用了 Refs 和 Atoms (后面會介紹)的例子。這個例子涉及到銀行賬戶以及賬戶之間的交易。首先我們定義一下數據模型。
```
(ns com.ociweb.bank)
; Assume the only account data that can change is its balance.
(defstruct account-struct :id wner :balance-ref)
; We need to be able to add and delete accounts to and from a map.
; We want it to be sorted so we can easily
; find the highest account number
; for the purpose of assigning the next one.
(def account-map-ref (ref (sorted-map)))
```
下面的函數建立一個新的帳戶,并且把它存入帳戶的map, ? 然后返回它。
```
(defn open-account
"creates a new account, stores it in the account map and returns it"
[owner]
(dosync ; required because a Ref is being changed
(let [account-map @account-map-ref
last-entry (last account-map)
; The id for the new account is one higher than the last one.
id (if last-entry (inc (key last-entry)) 1)
; Create the new account with a zero starting balance.
account (struct account-struct id owner (ref 0))]
; Add the new account to the map of accounts.
(alter account-map-ref assoc id account)
; Return the account that was just created.
account)))
```
下面的函數支持從一個賬戶里面存/取錢。
```
(defn deposit [account amount]
"adds money to an account; can be a negative amount"
(dosync ; required because a Ref is being changed
(Thread/sleep 50) ; simulate a long-running operation
(let [owner (account wner)
balance-ref (account :balance-ref)
type (if (pos? amount) "deposit" "withdraw")
direction (if (pos? amount) "to" "from")
abs-amount (Math/abs amount)]
(if (>= (+ @balance-ref amount) 0) ; sufficient balance?
(do
(alter balance-ref + amount)
(println (str type "ing") abs-amount direction owner))
(throw (IllegalArgumentException.
(str "insufficient balance for " owner
" to withdraw " abs-amount)))))))
(defn withdraw
"removes money from an account"
[account amount]
; A withdrawal is like a negative deposit.
(deposit account (- amount)))
```
下面是函數支持把錢從一個賬戶轉到另外一個賬戶。由 `dosync` 所開始的事務保證轉賬要么成功要么失敗,而不會出現中間狀態。
```
(defn transfer [from-account to-account amount]
(dosync
(println "transferring" amount
"from" (from-account wner)
"to" (to-account wner))
(withdraw from-account amount)
(deposit to-account amount)))
```
下面的函數支持查詢賬戶的狀態。由 `dosync` 所開始的事務保證事務之間的一致性。比如把不會報告一個轉賬了一半的金額。
```
(defn- report-1 ; a private function
"prints information about a single account"
[account]
; This assumes it is being called from within
; the transaction started in report.
(let [balance-ref (account :balance-ref)]
(println "balance for" (account wner) "is" @balance-ref)))
(defn report
"prints information about any number of accounts"
[& accounts]
(dosync (doseq [account accounts] (report-1 account))))
```
上面的代碼沒有去處理線程啟動時候可能拋出的異常。相反,我們在當前線程給他們定義了一個異常處理器。
```
; Set a default uncaught exception handler
; to handle exceptions not caught in other threads.
(Thread/setDefaultUncaughtExceptionHandler
(proxy [Thread$UncaughtExceptionHandler] []
(uncaughtException [thread throwable]
; Just print the message in the exception.
(println (.. throwable .getCause .getMessage)))))
```
現在我們可以調用上面的函數了。
```
(let [a1 (open-account "Mark")
a2 (open-account "Tami")
thread (Thread. #(transfer a1 a2 50))]
(try
(deposit a1 100)
(deposit a2 200)
; There are sufficient funds in Mark's account at this point
; to transfer $50 to Tami's account.
(.start thread) ; will sleep in deposit function twice!
; Unfortunately, due to the time it takes to complete the transfer
; (simulated with sleep calls), the next call will complete first.
(withdraw a1 75)
; Now there are insufficient funds in Mark's account
; to complete the transfer.
(.join thread) ; wait for thread to finish
(report a1 a2)
(catch IllegalArgumentException e
(println (.getMessage e) "in main thread"))))
```
上面代碼的輸出是這樣的:
```
depositing 100 to Mark
depositing 200 to Tami
transferring 50 from Mark to Tami
withdrawing 75 from Mark
transferring 50 from Mark to Tami (a retry)
insufficient balance for Mark to withdraw 50
balance for Mark is 25
balance for Tami is 200
```