iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 26
0
Software Development

擁抱 Clojure系列 第 26

[第 26 天] 擁抱 Clojure:並行與併發(三)

並行與併發(三)

狀態管理與併發

參考類型

Ref

Clojure 使用了軟體事務存儲 (Software Transactional Memory,之後簡稱 STM) 模型,來處理併發與狀態管理。STM 類似於資料庫,只是它存在於記憶體之中,僅能保證 ACID 中的三種:不可分割性 (Atomicity)、一致性 (Consistency) 與隔離性 (isolation),並不保證持久性 (Durability)。

在 Clojure 中,一旦進入改變狀態的事務交易 (Transaction) 環境裡,如果其中的變化有一個不成功,則會退出視爲失敗。而其它存取狀態的執行單元並不知道交易的狀態,只會看到交易前的情形。

而 Ref 參考類型就是 Clojure 根據 STM 的實作,透過 ref 函式創建內含有狀態的 Ref 類型:

(def account (ref 0))
;; => #'user/account

要取得參考類型的值,都是使用 deref 函式或小老鼠符號 @

(deref account)
;; => 0
(+ 5 @account)
;; => 5

你可以使用 ref-set 更新 Ref 狀態:

(ref-set account 500)
;; => IllegalStateException No transaction running

以上範例出現的例外告訴我們,更新 Ref 狀態必須在交易 (Transaction) 中進行。建立可以安心運作的交易環境使用 dosync

(dosync
 (ref-set account 500))
;; => 500
@account
;; => 500

現在 Ref 已更新狀態。除了 ref-set 之外,還提供以函式方式更新狀態的方法:

(dosync
 (alter account + 500))
;; => 1000
@account
;; => 1000

alter 的第一個參數是 Ref 類型,之後則是函式與準備帶給函式的參數,函式將會以下列的方式呼叫:

(apply fun value-in-ref args)

dosync 創造的交易環境確保在其中進行更新的多個 Ref,在交易完成之後將會同步更新。如果在交易之中,其中一個 Ref 的狀態已經被外部改變,整個交易便會重啓,利用新的狀態再進行改變。

(def debit (ref 100000))
(def account (ref 1000))
(dosync
 (alter debit - 1500)
 (alter account + 1500))
;; => 2500
@debit
;; => 98500
@account
;; => 2500

如果不想因爲交易內容改變而重啓,而且交易中的執行內容不會因爲順序改變而不同,則可以考慮 commute 函式:

(dosync
 (commute debit + 500)
 (commute account + 500))
;; => 3000
@debit
;; => 99000
@account
;; => 3000

將 Ref 添加驗證器 (Validator) 函式會確保在更新狀態的時候,符合驗證器函式的內容,若不符合則會返回之前的狀態:

(defn validate-account
  [state]
  (not (neg? state)))

(def bank-account (ref 1000 :validator validate-account))
(dosync
 (alter bank-account - 1500))
;; => IllegalStateException Invalid reference state
@bank-account
;; => 1000

Atom

原子類型 (Atom) 與 Ref 類型一樣都是屬於同步式:更新原子類型時必須等候先前的狀態更新完成,但是原子類型則不是協作式的,亦即無法同時更新兩個以上的原子類型,每個原子類型的更新都是與其他原子隔離的。

使用 atom 函式創造內含有狀態的原子類型,取得原子類型的值,也是使用 deref 函式或小老鼠符號 @

(def x (atom 10))
;; => #'user/x
@x
;; => 10

你可以使用 reset! 更新原子中的狀態:

(reset! x 20)
;; => 20
@x
;; => 20

也可以透過類似 alter 函式的 swap!,以更新函式來更新原子類型中的狀態:

(def catherine (atom {:name "Catherine" :age 18 :graduated? false}))
;; => #'user/catherine
(swap! catherine update-in [:age] + 2)
;; => {:name "Catherine", :age 20, :graduated? false}
(swap! catherine update-in [:graduate?] not)
;; => {:name "Catherine", :age 20, :graduated? true}
@catherine
;; => {:name "Catherine", :age 20, :graduated? true}

以上範例 swap!update-in 函式更新原子類型中的映射,它會以下面的呼叫方式更新映射:

(update-in @catherine [:age] + 2)

Clojure 提供了一個更新原子類型的函式,可以在更新之前先檢查狀態是否和預期的相等,若相等則更新至新值,否則不做改變:

(def x (atom 10))
(compare-and-set! x 20 30)
;; => false
@x
;; => 10
(compare-and-set! x 10 20)
;; => true
@x
;; => 20

原子類型也可以像 Ref 類型一樣添加驗證器,驗證欲改變的狀態是否符合驗證規則:

(def x (atom 100 :validator pos?))
(swap! x + 500)
;; => 600
(swap! x - 700)
;; => IllegalStateException Invalid reference state

除了驗證器之外,還可以對四種參考類型添加觀察者函式 (Watch function),一旦更新了參考類型的狀態,觀察者函式便會被呼叫。使用 add-watch 函式添加觀察者:

(add-watch x :echo
           (fn [key ref old new]
             (println "Key:" key)
             (println "Reference:" ref)
             (println "Old:" old)
             (println "New:" new)))
;; => #atom[600 0x1bce3aea]

add-watch 第一個參數是準備觀察的參考類型,第二個參數則是代表新的觀察者的關鍵字標識,你可以使用這個關鍵字來參照到新的觀察者函式。

最後一個參數是欲添加的觀察者函式,觀察者函式必須接受四個參數:觀察者的關鍵字標識、觀察的參考類型、舊狀態、新狀態。

觀察者函式加上去之後,狀態一旦改變,觀察者函式便會被呼叫:

(reset! x 300)
;; => Key: :echo
;; => Reference: #atom[300 0x1bce3aea]
;; => Old: 600
;; => New: 300
;; => 300

觀察完畢後,透過觀察者的關鍵字標識與函式 remove-watch,可以把觀察者函式從參考類型中移除:

(remove-watch x :echo)
;; => #atom[300 0x1bce3aea]
(reset! x 500)
;; => 500

從以上範例可以看到,觀察者函式移除之後,更新狀態就不會出現觀察者函式的訊息了。

(未完待續)


上一篇
[第 25 天] 擁抱 Clojure:並行與併發(二)
下一篇
[第 27 天] 擁抱 Clojure:並行與併發(四)
系列文
擁抱 Clojure30

尚未有邦友留言

立即登入留言