5

i am trying to get the "official" example of clojure concurrency closer to a java version using manual locking. In this gist i put the java and clojure code and the thread dump of a VisualVm profile of all versions. Here it is the clojure code and timing

(ns simple-example (:gen-class))
(set! *warn-on-reflection* true)
;; original from: http://clojure.org/concurrent_programming
(import '(java.util.concurrent Executors Future)
SimpleLocking$Node)

(defn test-concur [iter refs nthreads niters]
  (let [pool (Executors/newFixedThreadPool nthreads)
        tasks (map (fn [t]
                      (fn []
                        (dotimes [n niters]
                          (iter refs t))))
                   (range nthreads))]
    (doseq [^Future future (.invokeAll pool tasks)]
      (.get future))
    (.shutdown pool)))

(defn test-stm [nitems nthreads niters]
  (let [refs (vec (map ref (repeat nitems 0)))
        iter #(dosync (doseq [r %] (alter r + 1 %2)))]
    (test-concur iter refs nthreads niters)
    (map deref refs)))

(defn test-atom [nitems nthreads niters]
  (let [refs (vec (map atom (repeat nitems 0)))
        iter #(doseq [r %] (swap! r + 1 %2))]
    (test-concur iter refs nthreads niters)
    (map deref refs)))

;; SimpleLocking$Node is the class with the synchronized method of java version
(defn test-locking [nitems nthreads niters]
  (let [refs (->> (repeatedly #(SimpleLocking$Node.))
                    (take nitems) vec)
        iter #(doseq [^SimpleLocking$Node n %] (.sum n (+ 1 %2)))]
    (test-concur iter refs nthreads niters)
    (map (fn [^SimpleLocking$Node n] (.read n)) refs)))

(definterface INode
  (read [])
  (add [v]))

(deftype Node [^{:unsynchronized-mutable true} value]
  INode
  (read [_] value)
  (add [this v] (set! value (+ value v))))

(defn test-locking-native [nitems nthreads niters] 
  (let [refs (->> (repeatedly #(Node. 0))
          (take nitems) vec) 
    iter #(doseq [^Node n %]
          (locking n (.add n (+ 1 %2))))]
    (test-concur iter refs nthreads niters)
    (map (fn [^Node n] (.read n)) refs)))

(defn -main [& args]
  (read-line)
  (let [[type nitems nthreads niters] (map read-string args)
    t #(apply + (time (% nitems nthreads niters)))]
    (case type
      'lock (println "Locking:" (t test-locking)) 
      'atom (println "Atom:" (t test-atom))
      'stm (println "STM:" (t test-stm))
      'lock-native (println "Native locking:" (t test-locking-native)))))

Time (in an "old" intel core duo):

Java version
int nitems=100;
int nthreads=10;
final int niters=1000;
Sum node values: 5500000
Time: 31

simple-example=> (-main "lock" "100" "10" "1000")
"Elapsed time: 60.030324 msecs"
Locking: 5500000
nil
simple-example=> (-main "atom" "100" "10" "1000")
"Elapsed time: 202.309477 msecs"
Atom: 5500000
nil
simple-example=> (-main "stm" "100" "10" "1000")
"Elapsed time: 1830.568508 msecs"
STM: 5500000
nil
simple-example=> (-main "lock-native" "100" "10" "1000")
"Elapsed time: 159.730149 msecs"
Native locking: 5500000
nil

NOTE: I dont want get a clojure version as fast as java one, or a stm version as fast as clojure using locks one. I know that is in general difficult and with some problems impossible. I know the use of atoms and stm is more composable,easier to use and less error prone than using manual locks. Those version are only the best possible referents in java and clojure for the problem (well i did my best). My objective is get the atom and stm versions closer to locking ones, or to understand why (maybe in this concrete example) is not possible to speed up those versions.

NOTE: Another comparation, this time with haskell versions using STM and MVars (code in same gist linked):

>SimpleExampleMVar 100000 1000 6
Starting...
2100000000
Computation time: 11.781 sec
Done.

>SimpleExampleSTM 100000 1000 6
Starting...
2100000000
Computation time: 53.797 sec
Done.

>java -cp classes SimpleLocking
Sum node values: 2100000000
Time: 15.703 sec

java -cp classes;%CLOJURE_JAR% simple_example lock 1000 6 100000
"Elapsed time: 27.545 secs"
Locking: 2100000000

java -cp classes;%CLOJURE_JAR% simple_example lock-native 1000 6 100000
"Elapsed time: 80.913 secs"
Native locking: 2100000000

java -cp classes;%CLOJURE_JAR% simple_example atom 1000 6 100000
"Elapsed time: 95.143 secs"
Atom: 2100000000

java -cp classes;%CLOJURE_JAR% simple_example stm 1000 6 100000
"Elapsed time: 990.255 secs"
STM: 2100000000
4

1 回答 1

1

您并没有像这里那样真正进行比较 - Clojure 版本正在创建和交换新的不可变盒装数字,而 Java 版本只是int在同步方法中碰撞可变原始计数器。

您可以在 Clojure 中执行普通的 Java 风格的手动锁定,方法如下:

(locking obj (set! (. obj fieldName) (+ 1 (.fieldName obj)))))

locking构造实际上等效于 Javasynchronized代码块。

如果您使用类型提示的 Java 对象或带有:unsynchronized-mutable字段的 Clojure deftype 执行此操作,那么我认为您应该能够匹配纯 Java 同步性能。

尚未对此进行测试,但我认为它也应该与原语一起使用(如果您正在递增long计数器等,这可能很有用)

于 2012-09-19T02:27:53.867 回答