iT邦幫忙

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

擁抱 Clojure系列 第 27

[第 27 天] 擁抱 Clojure:並行與併發(四)

並行與併發(四)

狀態管理與併發

參考類型

Agent

有別於 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-offsend 提出的更新動作分別放入不同的佇列,分別由不同的執行緒集區 (Thread pool) 中的執行緒處理佇列,處理 send-off 動作佇列的執行緒集區會依據需要擴增,而處理 send 動作佇列的執行緒集區則是固定大小。

因此建議使用 send-off 處理會阻塞的操作,例如 IO。以免被過多的操作卡住無法從執行緒集區中分配新的執行緒來處理。

Var

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 給予建議與指教)


上一篇
[第 26 天] 擁抱 Clojure:並行與併發(三)
下一篇
[第 28 天] 擁抱 Clojure:巨集
系列文
擁抱 Clojure30

尚未有邦友留言

立即登入留言