Clojure
的并發(一) Ref和STM
Clojure 的并發(二)Write Skew分析
Clojure 的并發(三)Atom、緩存和性能
Clojure 的并發(四)Agent深入分析和Actor
Clojure 的并發(五)binding和let
Clojure的并發(六)Agent可以改進的地方
Clojure的并發(七)pmap、pvalues和pcalls
Clojure的并發(八)future、promise和線程
三、Atom和緩存
Ref適用的場景是系統中存在多個相互關聯的狀態,他們需要一起更新,因此需要通過dosync做事務包裝。但是如果你有一個狀態變量,不需要跟其他狀態變量協作,這時候應該使用Atom了。可以將一個Atom和一個Ref一起在一個事務里更新嗎?這沒辦法做到,如果你需要相互協作,你只能使用Ref。Atom適用的場景是狀態是獨立,沒有依賴,它避免了與其他Ref交互的開銷,因此性能會更好,特別是對于讀來說。
1、定義Atom,采用atom函數,賦予一個初始狀態:
這里將mem的初始狀態定義為一個map。
2、deref和@:可以用deref函數,也可以簡單地用宏@,這跟Ref一樣,取atom的值:
3、reset!:重新設置atom的值,不關心當前值是什么:
查看mem:
4、swap!:如果你的更新需要依賴當前的狀態值,或者只想更新狀態的某個部分,那么就需要使用swap!(類似alter):
swap! 將函數f作用于當前狀態值和額外的參數args之上,形成新的狀態值,例如我們給mem加上一個keyword:
看到,:b 2被加入了當前的map。
5、compare and set:
類似原子變量AtomicInteger之類,atom也可以做compare and set的操作:
當且僅當atom的當前狀態值等于oldValue的時候,將狀態值更新為newValue,并返回一個布爾值表示成功或者失敗:
6、緩存和atom:
(1)atom非常適合實現緩存,緩存通常不會跟其他系統狀態形成依賴,并且緩存對讀的速度要求更高。上面例子中用到的mem其實就是個簡單的緩存例子,我們來實現一個putm和getm函數:
這里key要求是keyword,keyword是類似:a這樣的字符序列,你熟悉ruby的話,可以暫時理解成symbol。使用這些API:
(2)memoize函數作用于函數f,產生一個新函數,新函數內部保存了一個緩存,緩存從參數到結果的映射。第一次調用的時候,發現緩存沒有,就會調用f去計算實際的結果,并放入內部的緩存;下次調用同樣的參數的時候,就直接從緩存中取,而不用再次調用f,從而達到提升計算效率的目的。
memoize的實現就是基于atom,查看源碼:
內部的緩存名為mem,memoize返回的是一個匿名函數,它接收原有的f函數的參數,if-let判斷綁定的變量e是否存在,變量e是通過find從緩存中查詢args得到的項,如果存在的話,調用val得到真正的結果并返回;如果不存在,那么使用apply函數將f作用于參數列表之上,計算出結果,并利用swap!將結果加入mem緩存,返回計算結果。
7、性能測試:
使用atom實現一個計數器,和使用java.util.concurrent.AtomicInteger做計數器,做一個性能比較,各啟動100個線程,每個線程執行100萬次原子遞增,計算各自的耗時,測試程序如下,代碼有注釋,不再羅嗦:
默認clojure調用java都是通過反射,加入type hint之后編譯的字節碼就跟java編譯器的一致,為了比較公平,定義了java-inc用于調用AtomicInteger.incrementAndGet方法,定義countdown-latch用于調用CountDownLatch.countDown方法,兩者都為參數添加了type hint。如果不采用type hint,AtomicInteger反射調用的效率是非常低的。
測試下來,在我的ubuntu上,AtomicInteger還是占優,基本上比atom的實現快上一倍:
按照我的理解,這是由于AtomicInteger調用的是native的方法,基于硬件原語做cas,而atom則是用戶空間內的clojure自己做的CAS,兩者的性能有差距不出意料之外。
看了源碼,Atom是基于java.util.concurrent.atomic.AtomicReference實現的,調用的方法是
而AtomicInteger調用的方法是:
兩者的效率差距有這么大嗎?暫時存疑。
Clojure 的并發(二)Write Skew分析
Clojure 的并發(三)Atom、緩存和性能
Clojure 的并發(四)Agent深入分析和Actor
Clojure 的并發(五)binding和let
Clojure的并發(六)Agent可以改進的地方
Clojure的并發(七)pmap、pvalues和pcalls
Clojure的并發(八)future、promise和線程
三、Atom和緩存
Ref適用的場景是系統中存在多個相互關聯的狀態,他們需要一起更新,因此需要通過dosync做事務包裝。但是如果你有一個狀態變量,不需要跟其他狀態變量協作,這時候應該使用Atom了。可以將一個Atom和一個Ref一起在一個事務里更新嗎?這沒辦法做到,如果你需要相互協作,你只能使用Ref。Atom適用的場景是狀態是獨立,沒有依賴,它避免了與其他Ref交互的開銷,因此性能會更好,特別是對于讀來說。
1、定義Atom,采用atom函數,賦予一個初始狀態:
(def mem (atom {}))
這里將mem的初始狀態定義為一個map。
2、deref和@:可以用deref函數,也可以簡單地用宏@,這跟Ref一樣,取atom的值:
@mem => {}
(deref mem) => {}
(deref mem) => {}
3、reset!:重新設置atom的值,不關心當前值是什么:
(reset! mem {:a 1})
查看mem:
user=> @mem
{:a 1}
已經更新到新的map了。{:a 1}
4、swap!:如果你的更新需要依賴當前的狀態值,或者只想更新狀態的某個部分,那么就需要使用swap!(類似alter):
(swap! an-atom f & args)
swap! 將函數f作用于當前狀態值和額外的參數args之上,形成新的狀態值,例如我們給mem加上一個keyword:
user=> (swap! mem assoc :b 2)
{:b 2, :a 1}
{:b 2, :a 1}
看到,:b 2被加入了當前的map。
5、compare and set:
類似原子變量AtomicInteger之類,atom也可以做compare and set的操作:
(compare-and-set! atom oldValue newValue)
當且僅當atom的當前狀態值等于oldValue的時候,將狀態值更新為newValue,并返回一個布爾值表示成功或者失敗:
user=> (def c (atom 1))
#'user/c
user=> (compare-and-set! c 2 3)
false
user=> (compare-and-set! c 1 3)
true
user=> @c
3
#'user/c
user=> (compare-and-set! c 2 3)
false
user=> (compare-and-set! c 1 3)
true
user=> @c
3
6、緩存和atom:
(1)atom非常適合實現緩存,緩存通常不會跟其他系統狀態形成依賴,并且緩存對讀的速度要求更高。上面例子中用到的mem其實就是個簡單的緩存例子,我們來實現一個putm和getm函數:
;;創建緩存
(defn make-cache [] (atom {}))
;;放入緩存
(defn putm [cache key value] (swap! cache assoc key value))
;;取出
(defn getm [cache key] (key @cache))
(defn make-cache [] (atom {}))
;;放入緩存
(defn putm [cache key value] (swap! cache assoc key value))
;;取出
(defn getm [cache key] (key @cache))
這里key要求是keyword,keyword是類似:a這樣的字符序列,你熟悉ruby的話,可以暫時理解成symbol。使用這些API:
user=> (def cache (make-cache))
#'user/cache
user=> (putm cache :a 1)
{:a 1}
user=> (getm cache :a)
1
user=> (putm cache :b 2)
{:b 2, :a 1}
user=> (getm cache :b)
2
#'user/cache
user=> (putm cache :a 1)
{:a 1}
user=> (getm cache :a)
1
user=> (putm cache :b 2)
{:b 2, :a 1}
user=> (getm cache :b)
2
(2)memoize函數作用于函數f,產生一個新函數,新函數內部保存了一個緩存,緩存從參數到結果的映射。第一次調用的時候,發現緩存沒有,就會調用f去計算實際的結果,并放入內部的緩存;下次調用同樣的參數的時候,就直接從緩存中取,而不用再次調用f,從而達到提升計算效率的目的。
memoize的實現就是基于atom,查看源碼:
(defn memoize
[f]
(let [mem (atom {})]
(fn [& args]
(if-let [e (find @mem args)]
(val e)
(let [ret (apply f args)]
(swap! mem assoc args ret)
ret)))))
[f]
(let [mem (atom {})]
(fn [& args]
(if-let [e (find @mem args)]
(val e)
(let [ret (apply f args)]
(swap! mem assoc args ret)
ret)))))
內部的緩存名為mem,memoize返回的是一個匿名函數,它接收原有的f函數的參數,if-let判斷綁定的變量e是否存在,變量e是通過find從緩存中查詢args得到的項,如果存在的話,調用val得到真正的結果并返回;如果不存在,那么使用apply函數將f作用于參數列表之上,計算出結果,并利用swap!將結果加入mem緩存,返回計算結果。
7、性能測試:
使用atom實現一個計數器,和使用java.util.concurrent.AtomicInteger做計數器,做一個性能比較,各啟動100個線程,每個線程執行100萬次原子遞增,計算各自的耗時,測試程序如下,代碼有注釋,不再羅嗦:
(ns atom-perf)
(import 'java.util.concurrent.atomic.AtomicInteger)
(import 'java.util.concurrent.CountDownLatch)
(def a (AtomicInteger. 0))
(def b (atom 0))
;;為了性能,給java加入type hint
(defn java-inc [#^AtomicInteger counter] (.incrementAndGet counter))
(defn countdown-latch [#^CountDownLatch latch] (.countDown latch))
;;單線程執行緩存次數
(def max_count 1000000)
;;線程數
(def thread_count 100)
(defn benchmark [fun]
(let [ latch (CountDownLatch. thread_count) ;;關卡鎖
start (System/currentTimeMillis) ] ;;啟動時間
(dotimes [_ thread_count] (.start (Thread. #(do (dotimes [_ max_count] (fun)) (countdown-latch latch)))))
(.await latch)
(- (System/currentTimeMillis) start)))
(println "atom:" (benchmark #(swap! b inc)))
(println "AtomicInteger:" (benchmark #(java-inc a)))
(println (.get a))
(println @b)
(import 'java.util.concurrent.atomic.AtomicInteger)
(import 'java.util.concurrent.CountDownLatch)
(def a (AtomicInteger. 0))
(def b (atom 0))
;;為了性能,給java加入type hint
(defn java-inc [#^AtomicInteger counter] (.incrementAndGet counter))
(defn countdown-latch [#^CountDownLatch latch] (.countDown latch))
;;單線程執行緩存次數
(def max_count 1000000)
;;線程數
(def thread_count 100)
(defn benchmark [fun]
(let [ latch (CountDownLatch. thread_count) ;;關卡鎖
start (System/currentTimeMillis) ] ;;啟動時間
(dotimes [_ thread_count] (.start (Thread. #(do (dotimes [_ max_count] (fun)) (countdown-latch latch)))))
(.await latch)
(- (System/currentTimeMillis) start)))
(println "atom:" (benchmark #(swap! b inc)))
(println "AtomicInteger:" (benchmark #(java-inc a)))
(println (.get a))
(println @b)
默認clojure調用java都是通過反射,加入type hint之后編譯的字節碼就跟java編譯器的一致,為了比較公平,定義了java-inc用于調用AtomicInteger.incrementAndGet方法,定義countdown-latch用于調用CountDownLatch.countDown方法,兩者都為參數添加了type hint。如果不采用type hint,AtomicInteger反射調用的效率是非常低的。
測試下來,在我的ubuntu上,AtomicInteger還是占優,基本上比atom的實現快上一倍:
atom: 9002
AtomicInteger: 4185
100000000
100000000
AtomicInteger: 4185
100000000
100000000
按
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
而AtomicInteger調用的方法是:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
兩者的效率差距有這么大嗎?暫時存疑。