有別於 Ref 與原子類型的協調式與同步式,Agent 類型狀態的更新不需與其他狀態更新協同合作,也不需等候其他更新完成。通常用在需要更新狀態,卻不需要關心更新後的結果。
使用 atom
函式創造內含有狀態的 Agent 類型,取得 Agent 類型的值,也是使用 deref
函式或小老鼠符號 @
:
(def a (agent 5))
;; =>#'user/a
@a
;; => 5
可以使用 send
函式更新 Agent 類型中的狀態,使用方法與 alter
以及 swap!
類似:
(send a + 100)
;; => #agent[{:status :ready, :val 105} 0x11fdbaa4]
@a
;; => 105
不同於 Ref 與原子類型更新函式的返回值,send
返回的是狀態更新後的 Agent 類型。
另外還有 send-off
提供與 send
函式同樣的使用方法:
(send-off a + 150)
;; => #agent[{:status :ready, :val 255} 0x11fdbaa4]
@a
255
Agent 類型會將更新狀態的動作放入佇列中,啓動執行緒逐個處理。send-off
與 send
提出的更新動作分別放入不同的佇列,分別由不同的執行緒集區 (Thread pool) 中的執行緒處理佇列,處理 send-off
動作佇列的執行緒集區會依據需要擴增,而處理 send
動作佇列的執行緒集區則是固定大小。
因此建議使用 send-off
處理會阻塞的操作,例如 IO。以免被過多的操作卡住無法從執行緒集區中分配新的執行緒來處理。
Var 物件已經是我們非常熟悉的參考類型。使用 def
創建內含資料的 Var 物件:
(def xx 1)
建立了 Var 物件之後,Var 物件被賦予的值被稱爲根繫結 (root binding),它是全域可見的,所有的執行緒都可以取得:
(.start
(Thread.
(fn [] (println "xx:" xx))))
;; => xx: 1
若在創建 Var 物件時加上特殊的詮釋資料 (Metadata),便可以動態改變 Var 物件的內容:
(def ^:dynamic *xx* 111)
(binding [*xx* 222]
*xx*)
;; => 222
*xx*
;; => 111
以上範例使用 binding
在其本體運算式內重新繫結了 Var 物件,因此在 binding
作用區之內,Var 物件的內容值改變了,一旦離開作用區域,Var 物件又回到根繫結的值。
使用 alter-var-root
可以將 Var 物件的根繫結換成新的值:
(.start
(Thread.
(fn []
(alter-var-root (var *xx*) (fn [_] 333))
(println "*xx* is now" *xx*))))
;; => *xx* is now 333
*xx*
;; => 333
alter-var-root
接受一個將被改變根繫結的 Var 物件,以及一個函式,該函式的返回值便是此 Var 物件根繫結的新值。
Clojure 不建議在並行或併發的環境下使用 Var 物件做資源的共享,因爲各執行緒之間無法做好協調。執行緒 A 中取得的值,可能在執行緒 B 中已經做了修改。
經由本篇文章,你學會了 Clojure 支援並行的方式,以及與併發之間的差別。還知道了如何以並行方式更快完成工作,也學會了如何使用四種參考型別來管理狀態,以及他們之間的特色與差別。
還不賴吧?今天就先到這裡,下一篇文章再見囉!
(本篇文章同步刊登於 GitHub,歡迎在文章下方留言或發送 PR 給予建議與指教)