Estoy tratando de obtener the "official" example of clojure concurrency más cerca de una versión Java con bloqueo manual. In this gist puse el código java y clojure y el volcado de subprocesos de un perfil VisualVm de todas las versiones. aquí está el código clojure y el momentoPosibles mejoras en el ejemplo de concurrencia "oficial" de clojure (usando bloqueos, átomos, stm)
(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)))))
Tiempo (en un "viejo" Core Duo de Intel):
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
NOTA: No quiero obtener una versión clojure tan rápido como java uno, o una versión stm tan rápido como clojure usando bloqueos uno. Sé que en general es difícil y con algunos problemas imposibles. Sé que el uso de átomos y stm es más composable, más fácil de usar y menos propenso a errores que el uso de bloqueos manuales. Esas versiones son solo los mejores referentes posibles en java y clojure para el problema (bueno, hice mi mejor esfuerzo). Mi objetivo es acercar las versiones atom y stm a las de bloqueo, o comprender por qué (quizás en este ejemplo concreto) no es posible acelerar esas versiones.
NOTA: Otra comparacion, esta vez con versiones Haskell usando STM y MVARs (código en el mismo meollo vinculados):
>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
de Clojure son herramientas para comprometer el rendimiento con el fin de lograr buenas abstracciones que tienen una semántica consistente en todas partes. El bloqueo manual que se implementa correctamente será más rápido casi todo el tiempo, pero generalmente es más difícil obtener la semántica de bloqueo correcta que utilizar Átomos/Refs/Agentes correctamente. – animal
La pregunta está relacionada de alguna manera con esta (una versión de haskell): http://stackoverflow.com/questions/12475363/speed-up-haskell-concurrency – jneira
@animal Soy consciente de esa compensación, pero la diferencia entre stm y átomos o bloqueo es demasiado grande. Tal vez el problema no es adecuado para el STM optimista? – jneira