並行程式設計
現今的系統必須處理許多同時進行的任務,並利用多核心 CPU 的效能。由於同步的複雜性,使用執行緒來執行此操作可能會非常困難。Clojure 以多種方式簡化多執行緒程式設計。由於核心資料結構是不可變的,因此它們可以在執行緒之間輕鬆地共用。然而,在程式中經常需要變更狀態。Clojure 是一種實用的語言,允許變更狀態,但提供機制以確保在變更狀態時保持一致性,同時讓開發人員不必手動使用鎖定等方式來避免衝突。軟體交易記憶體系統 (STM) 透過 dosync、ref、ref-set、alter 等公開,支援以同步且協調的方式在執行緒之間共用變更狀態。代理系統支援以非同步且獨立的方式在執行緒之間共用變更狀態。原子系統支援以同步且獨立的方式在執行緒之間共用變更狀態。動態變數系統透過 def、binding 等公開,支援在執行緒內隔離變更狀態。
在所有情況下,Clojure 都不會取代 Java 執行緒系統,而是與它一起工作。Clojure 函數為 java.util.concurrent.Callable,因此它們與 Executor 架構等一起工作。
Ref 是物件的可變參考。它們可以在交易期間 ref-set 或 alter 為參考不同的物件,這些交易由 dosync 區塊分隔。交易中的所有 ref 修改都會發生,否則都不會發生。此外,交易中的 ref 讀取會反映特定時間點的參考世界快照,即每個交易都與其他交易隔離。如果嘗試修改相同參考的 2 個交易之間發生衝突,其中一個交易會重試。所有這些都會在沒有明確鎖定的情況下發生。
在此範例中,會建立包含整數的 Ref 向量 (refs),然後設定一組執行緒 (pool) 來執行遞增每個 Ref 的多次反覆運算 (tasks)。這會產生極端的競爭,但會產生正確的結果。沒有鎖定!
(import '(java.util.concurrent Executors))
(defn test-stm [nitems nthreads niters]
(let [refs (map ref (repeat nitems 0))
pool (Executors/newFixedThreadPool nthreads)
tasks (map (fn [t]
(fn []
(dotimes [n niters]
(dosync
(doseq [r refs]
(alter r + 1 t))))))
(range nthreads))]
(doseq [future (.invokeAll pool tasks)]
(.get future))
(.shutdown pool)
(map deref refs)))
(test-stm 10 10 10000)
-> (550000 550000 550000 550000 550000 550000 550000 550000 550000 550000)
在一般用法中,ref 可以參考 Clojure 集合,這些集合因為是持續且不可變的,所以能有效支援多個交易同時進行推測性的「修改」。不應將可變物件放入 ref 中。
預設情況下,Vars 是靜態的,但使用 metadata 定義的 Vars 的每個執行緒繫結會將它們標記為動態的。 動態變數 也是物件的可變參考。它們有一個 (執行緒共用) 根繫結,可透過 def 建立,並可以使用 *set!* 設定,但前提是它們已使用 binding 在執行緒本機繫結到新的儲存位置。這些繫結和對這些繫結的任何後續修改都只會在繫結區塊的動態範圍內的程式碼中 在 執行緒 內 可見。巢狀繫結遵循堆疊協定,並在控制離開繫結區塊時解除。
(def ^:dynamic *v*)
(defn incv [n] (set! *v* (+ *v* n)))
(defn test-vars [nthreads niters]
(let [pool (Executors/newFixedThreadPool nthreads)
tasks (map (fn [t]
#(binding [*v* 0]
(dotimes [n niters]
(incv t))
*v*))
(range nthreads))]
(let [ret (.invokeAll pool tasks)]
(.shutdown pool)
(map #(.get %) ret))))
(test-vars 10 1000000)
-> (0 1000000 2000000 3000000 4000000 5000000 6000000 7000000 8000000 9000000)
(set! *v* 4)
-> java.lang.IllegalStateException: Can't change/establish root binding of: *v* with set
動態變數提供一種在呼叫堆疊上的不同點之間進行通訊的方法,而不會污染中間呼叫的引數清單和傳回值。此外,動態變數支援一種面向情境的程式設計。因為使用 defn 定義的函數儲存在變數中,所以它們也可以動態重新繫結
(defn ^:dynamic say [& args]
(apply str args))
(defn loves [x y]
(say x " loves " y))
(defn test-rebind []
(println (loves "ricky" "lucy"))
(let [say-orig say]
(binding [say (fn [& args]
(println "Logging say")
(apply say-orig args))]
(println (loves "fred" "ethel")))))
(test-rebind)
ricky loves lucy
Logging say
fred loves ethel