iT邦幫忙

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

擁抱 Clojure系列 第 25

[第 25 天] 擁抱 Clojure:並行與併發(二)

並行與併發(二)

並行

pmap

先前的章節已經看過的 map 函式,功能是將群集的各個元素套用到函式之中,產生新的群集。如果被套用的函式需要長時間的運算,等待所有元素都計算完畢就耗時過久。

pmap 函式 (Parallel map) 提供升級的 map 功能,將每個元素的運算分給不同的執行緒,所有元素計算完畢再彙整起來,如果有夠多的計算核心,完成的時間越縮短。

以下的範例中有一個模擬運行耗時十秒的運算,分別套用到有十個元素的群集,使用 pmap 比起 map 效能提升顯著:

(def data [2 4 6 8 10 12 14 16 18 20])
(defn long-computaion [n]
  (Thread/sleep 10000)
  (* n 2))

(time (dorun (map long-computaion data)))
;; => "Elapsed time: 100031.777566 msecs"
(time (dorun (pmap long-computaion data)))
;; => "Elapsed time: 10027.629015 msecs"

可以看到以上範例中,原來的 map 版本以約略於 100 秒的時間完成,而進化的 pmap 版本由於受益於並行化,以近似 10 秒的時間完成。

範例中使用 dorun 強制對 map 返回的惰性序列求值,並以 time 函式計算運行花費的時間。

另外還有 pvalues 以及 pcalls 分別並行地對多個運算式求值,以及呼叫多個函式:

(pvalues (+ 3 2) (/ 2 3) (* 3 2) (- 32 23))
;; => (5 2/3 6 9)
(pcalls #(println "A long time ago in a galaxy far,") #(println "far away") #(println "...."))
;; => A long time ago in a galaxy far,
;; => far away …
;; => (nil nil nil).

Reducer

核心函式庫中的 mapfilterreduce (它還有另外一個名字:fold) 的作用是將一個群集轉換成另一個群集,雖然返回的是惰性序列仍然需要有創建的成本。

不同於核心函式庫的 reducer 函式庫,轉換的則不是資料結構,而是函式。不需要在一連串函式的轉換過程中創建暫時性的序列,而只是轉換運行的函式,此舉將會大大地增加效能。

clojure.core.reducer 函式中的 mapfilter 函式並不回傳惰性序列,而是傳回屆時可以做化約 (reducible) 的函式,稱爲 reducer

其中使用了 Java 7 中用以執行並行任務的框架:Fork/Join,將一連串的計算函式並行處理,減少處理時間。以下是典型的 map 與使用 reducer 後的各別效能評比:

(require '[clojure.core.reducers :as r])

(defn old-reduce [nums]
  (reduce + (filter even? (map inc nums))))
(defn new-fold [nums]
  (r/fold + (r/filter even? (r/map inc nums))))

(time (old-reduce (vec (range 1000000))))
;; => "Elapsed time: 136.409418 msecs"
;; => 250000500000
(time (new-fold (vec (range 1000000))))
;; => "Elapsed time: 96.708929 msecs"
;; => 250000500000

狀態管理與併發

併發 (Concurrency) 是指同時有數個執行單元會交互執行,通常會牽涉一些共享的資源以及互相協作。

其實 Clojure 並沒有提供併發相關的函式或巨集,它提供了經過妥善設計的狀態管理方法,讓不同執行單元之間共享資源更容易管理且不易出錯。

在介紹狀態管理方法之前,先來了解 Clojure 對於事物的世界觀。

身份與狀態

在 Clojure 世界中,一件事物分成身份 (Identity) 與狀態 (State),在時間的長河裡,每件事物在不同的時間有不同的狀態,狀態以值表示,由於值在 Clojure 世界中具有不變性,因此無法對值進行改變。如果要取得事物的狀態,則必須透過身份來取得,但是取得的狀態只是某個時間中狀態的快照 (Snapshot)。

例如一位名爲 Catherine 的使用者,20 歲時剛畢業開始工作,30 歲時結婚,不同的時間有不同的狀態,但是都是同一個身份。如果在傳統的程式語言,身份與狀態是含混不清的:

catherine.age = 20;
catherine.graduated = true;
;; 時間經過十年...
catherine.age += 10;
catherine.married = true;

上面的範例中,一個使用者類型既是代表某一種身份,更混雜了狀態。在 Clojure 中,狀態儲存在四種參考類型中,透過函式取得其中的狀態,新的狀態也是經由函式加上舊的狀態產生而成。而新的狀態在新的時間中,並不會影響其它時間的狀態。

若是有人在 20 秒前取得某個身份的狀態,10 秒後這個身份改變了狀態,之前取得的狀態並不會改變,保證了時間軸上狀態的一致。

參考類型

Clojure 使用四種類型管理狀態,這些類型稱爲參考類型 (Reference type)。四種參考類型分別又隸屬於兩種分類:協作式 (Coordinated) 與同步式 (Synchronous),協作式指的是不同狀態更新時需要協調合作,同步式則是指在更新狀態前有可能會被阻攔或停滯,因爲其他部分正在更新,所以必須等待。

以下以圖形表示參考類型所屬的分類:

;;       | Coordinated | Uncoordinated
;; ------|-------------|--------------
;; Sync  |    Ref      |    Atom
;; Async |    N/A      |    Agent

(未完待續)


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

尚未有邦友留言

立即登入留言