【问题标题】:Possible improvements in clojure "official" concurrency example (using locks,atoms,stm)clojure“官方”并发示例的可能改进(使用锁、原子、stm)
【发布时间】:2012-09-18 20:11:10
【问题描述】:

我正在尝试使用手动锁定使 the "official" example of clojure concurrency 更接近 java 版本。 In this gist 我放了所有版本的 VisualVm 配置文件的 java 和 clojure 代码以及线程转储。 这是clojure代码和时间

(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)))))

时间(在“旧”英特尔核心二重奏中):

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

注意:我不想得到一个和 java 一样快的 clojure 版本,或者像使用锁的 clojure 一样快的 stm 版本。我知道这通常很困难,而且有些问题是不可能的。我知道使用 atom 和 stm 比使用手动锁更易于组合、更易于使用且不易出错。这些版本只是 java 和 clojure 中问题的最佳参考(我已经尽力了)。 我的目标是让 atom 和 stm 版本更接近于锁定版本,或者理解为什么(可能在这个具体示例中)无法加速这些版本。

注意:另一个比较,这次是使用 STM 和 MVars 的 haskell 版本(链接相同 gist 中的代码):

>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

【问题讨论】:

  • Clojure 的规范并发编程结构是为了实现在各处具有一致语义的良好抽象而牺牲性能的工具。正确实现的手动锁定几乎每次都会更快,但通常比正确使用 Atoms/Refs/Agents 更难获得正确的锁定语义。
  • 这个问题与这个(haskell 版本)有点相关:stackoverflow.com/questions/12475363/…
  • @animal 我知道这种权衡,但 imo stm 和 atom 或锁定之间的差异太大。也许这个问题不适合乐观的 STM?
  • 我认为问题的一个重要来源是 SimpleLocking$Node 走的比原子更快的路径。 SimpleLocking$Node 有一个固定操作,即在同步访问下以 x 递增。使用 Atoms,必须读取初始值,将 fn 计算的新值传递给 swap!,然后需要成功执行原子比较和交换操作。 SimpleLocking$Node 上的争用会导致线程阻塞。对原子的争用导致需要重复调​​用的更改 fn。后者无法与前者竞争。
  • 有两种方法可以获得更多的苹果对苹果的比较:让 SimpleLocking$Node 接受一个可运行的,读取它的值,将可运行的应用到它的值,然后对读取的值进行 CAS。或者,因为修改竞争资源的线程都没有直接读取它,并且只向它推送更改,它是代理的一个很好的候选者,它应该获得接近同步场景的性能。

标签: performance concurrency clojure jvm stm


【解决方案1】:

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

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

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

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

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

尚未对此进行测试,但我认为它也应该适用于原语(如果您增加 long 计数器等,这可能很有用)

【讨论】:

  • 我不确定主要问题是否与拳击数字有关(尽管它会增加一些开销)。查看 CPU 配置文件,您可以看到 atom 版本确实 2007778 添加调用,stm 版本 2852124 和锁定版本(java 和 clojure)1000000 固定调用。此外,stm 版本在阻塞线程上花费的时间比其他版本更多。也许这个具体问题(官方!)引发了太多的冲突和重试。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-12-11
  • 1970-01-01
  • 2011-02-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多