建構軟體設計有兩種方式:
一種是簡單明顯地沒有缺陷,另一種則是複雜到沒有明顯的缺陷。— 東尼•霍爾
現代計算機系統走向多核,爲了運用多核心的能力,開始利用程式語言或作業系統提供的執行緒及處理序,將任務切割後同時處理。而不同任務間如果有共同的資源需要維護,增加了編寫程式人員的負擔。
一般的程式語言會使用鎖或是互斥器,避免不同程式同時存取相同資源,但是一旦使用不當便會發生死鎖或競爭條件等問題。而不同程式之間共享同一資源,更有可能造成兩邊資訊更新不一致。
Clojure 中的不變性 (Immutable) 與持久存在 (Persistent),讓資料結構一旦建立就無法再更改並保持一定的效能,降低開發者面對錯綜複雜更新的風險。而設計巧妙的狀態管理,更減輕了開發者漸強的偏頭痛。
先來看看 Clojure 如何支援並行 (Parallelism)。
並行 (Parallelism) 指的是同時有不同的程式分別去做各自任務,任務完成之後,將結果彙整起來。如果運行的環境具備多核心的能力,則任務便可以在不同的核心上執行,完成的時間將會減少。
既然 Clojure 建基在 Java 之上,自然可以使用 Java 中的執行緒類別,又由於 Clojure 中的函式實作了 Callable 與 Runnable 介面,使用起來自是容易許多:
(.start
(Thread.
(fn []
(Thread/sleep 3000)
(println "Thread ends."))))
;; => nil
;; 等待三秒
;; => Thread ends.
Clojure 提供了更簡便的方式讓你將函式置於執行緒中運行。使用 future
將欲完成的任務置於另一個執行緒中運行,呼叫 future
會返回 Future 物件並開始運行任務。
當任務完成後返回值則存放在 Future 物件之中,取得返回值則使用標的函式 deref
或小老鼠符號 @
於 Future 物件,即可取得任務執行後的結果。如任務尚未完成欲取得返回值,則會等待其計算完畢:
(def f (future (Thread/sleep 10000) (println "done") 100))
@f ;; 若在十秒內,此行將會停住等待計算完畢
;; => 100
你可以使用 future-done?
檢查一個 Future
物件是否完成運行:
(def f (future (Thread/sleep 10000) (println "done") 100))
(future-done? f) ;; 十秒內運行
;; => false
;; 十秒後
(future-done? f)
;; => true
使用 future-cancel
則會將一個已經開始運行的 Future 物件終止,如果對一個已經被強制終止的 Future 物件取用標的 (deref),會拋出例外:
(def f (future (Thread/sleep 10000) (println "done") 100))
(future-cancel f)
;; => true
@f
;; => CancellationException
利用 promise
建立的 Promise 物件則是與呼叫者建立約定,結果計算完畢之後會將它發送給持有 Promise 物件者。使用 promise
建立 Promise 物件、使用 deliver
傳送結果給 Promise 物件:
(def answer (promise))
(future (Thread/sleep 10000) (deliver answer 42))
;; => #future[{:status :pending, :val nil} 0x2b9dc292]
@answer ;; 十秒內運行的話,此行會停住
;; => 42
與 Future 一樣,若 Promise 物件尚未接收到結果,取用標的 Promise 物件將會停住等待結果送到。你可以使用 realized?
於 Promise 物件上,來取得結果是否已送達:
(def p (promise))
(realized? p)
;; => false
(deliver p :done)
(realized? p)
;; => true
(未完待續)