为什么在另一个线程中循环处理大量数据会导致GC过度活动,并阻止某些数据被释放?

时间:2018-08-09 22:41:04

标签: multithreading concurrency clojure garbage-collection

我正在编写代码,以获取pmap产生的一些惰性结果,并将它们绘制到BufferedImage上。现在三天来,我一直在试图弄清楚为什么图纸突然开始冻结,并最终停滞了整个过程的1/3。

我终于将其范围缩小到我在另一个线程中循环处理大量数据的事实。

这是我想出的最好的MCVE:

(ns mandelbrot-redo.irrelevant.write-image-mcve
  (:import [java.awt.image BufferedImage]
           (java.util.concurrent Executors Executor)))

(defn lazy-producer [width height]
  (for [y (range height)
        x (range width)]
    [x y (+ x y)]))

; This works fine; finishing after about 5 seconds when width=5000
(defn sync-consumer [results width height]
  (time
    (doseq [[i result] (map vector (range) results)]
      (when (zero? (rem i 1e6))
        (println (str (double (/ i (* width height) 0.01)) "%")))

      ((fn boop [x] x) result)))) ; Data gets consumed here

; This gets to ~30%, then begins being interupted by 1-4 second lags
(defn async-consumer [results width height]
  (doto
    (Thread. ^Runnable
             (fn []
               (sync-consumer results width height)
               (println "Done...")))
    (.start)))

(defn -main []
  (let [width 5000
        height (int (* width 2/3))]
    (-> (lazy-producer width height)
        (async-consumer width height))))

-mainsync-consumer一起运行时,它会在几秒钟后结束。但是,使用async-consumer时,它会达到25%左右,然后开始变慢,直到最后打印的百分比为30%。如果离开它,我会得到一个OOME。

如果我使用显式的Thread.,或者在async-consumer中使用了本地线程池,则它会挂起并崩溃。但是,如果我使用future,它会像sync-consumer一样正常。

我得到的唯一提示是,当我在VisualVM中运行此命令时,我看到使用异步版本时Long的分配失控了:

VisualVM Snapshot

相比之下,同步版本显示Long的峰值数量一次约为45mb。

CPU使用率也大不相同:

enter image description here

GC大量增加,但似乎Long并没有被丢弃。

可以为此使用future,但是我被它的异常吞咽行为咬伤了很多次,我很犹豫。

为什么会这样?为什么在新线程中运行它会导致GC疯狂,而同时没有释放数字?

任何人都可以解释这种行为吗?

3 个答案:

答案 0 :(得分:2)

同步版本似乎正在处理16M +结果,并且由于本地清除而无法保持结果序列的开头。这意味着在您进行操作时,将创建,处理和GC值。

异步程序在fn中关闭results并保持头部,将所有16M +值保留在内存中,可能导致GC崩溃吗?

我实际上无法复制您描述的内容-同步和异步对我来说大约需要花费相同的时间,如上所述。 (Clojure 1.9,Java 1.8)。

答案 1 :(得分:0)

我简化了您的示例,并得到不一致的结果。我怀疑手动Thread对象有时被视为守护线程,因此JVM有时会在完成之前退出:

(def N 5e3)
(def total-count (* N N))
(def report-fact (int (/ total-count 20)))

(defn lazy-producer []
  (for [y (range N)
        x (range N)]
    [x y (+ x y)]))

(defn sync-consumer [results]
  (println "sync-consumer:  start")
  (time
    (doseq [[i result] (map vector (range) results)]
      (when (zero? (rem i report-fact))
        (println (str (Math/round (/ (* 100 i) total-count)) " %")))))
  (println "sync-consumer:  stop"))

 (defn async-consumer [results]
  ; (spyx (count results))
  (spyx (nth results 99))
  (let [thread (Thread. (fn []
                          (println "thread  start")
                          (sync-consumer results)
                          (println "thread  done")
                          (flush)))]
    ; (.setDaemon thread false)
    (.start thread)
    (println "daemon?   " (.isDaemon thread))
    thread))

(dotest
  (println "test - start")
  (let [thread (async-consumer
                 (lazy-producer))]
    (when true
      (println "test - sleeping")
      (Thread/sleep 5000))
 ;  (.join thread)
  )
  (println "test - end"))

有结果:

----------------------------------
   Clojure 1.9.0    Java 10.0.1
----------------------------------

lein test tst.demo.core
test - start
(nth results 99) => [99 0 99]
daemon?    false
test - sleeping
thread  start
sync-consumer:  start
0 %
5 %
10 %
15 %
20 %
25 %
30 %
35 %
40 %
45 %
50 %
55 %
test - end

Ran 2 tests containing 0 assertions.
0 failures, 0 errors.
60 %
lein test  54.58s user 1.37s system 372% cpu 15.028 total

如果我们取消对(.join thread)行的注释,我们将获得完整的运行:

~/expr/demo > lein test

----------------------------------
   Clojure 1.9.0    Java 10.0.1
----------------------------------

lein test tst.demo.core
test - start
(nth results 99) => [99 0 99]
daemon?    false
test - sleeping
thread  start
sync-consumer:  start
0 %
5 %
10 %
15 %
20 %
25 %
30 %
35 %
40 %
45 %
50 %
55 %
60 %
65 %
70 %
75 %
80 %
85 %
90 %
95 %
"Elapsed time: 9388.313828 msecs"
sync-consumer:  stop
thread  done
test - end

Ran 2 tests containing 0 assertions.
0 failures, 0 errors.
lein test  72.52s user 1.69s system 374% cpu 19.823 total

仿佛Clojure杀死了手动Thread对象一样,它似乎早已退出。

也许您发现了(间歇性)错误。

答案 2 :(得分:0)

多亏了@amalloy和@Alex,我才能够正常工作。

我在评论中实现了@amalloy的建议,这两种变体在这里和在我的实际情况下都有效:

; Brittle since "once" is apparently more of an implementation detail of the language
(defn async-consumer [results width height]
  (doto
    (Thread. ^Runnable
             (^:once fn* []
               (sync-consumer results width height)
               (println "Done...")))
    (.start)))

; Arguably less brittle under the assumption that if they replace "once" with another mechanism,
;  they'll update "delay".
(defn async-consumer [results width height]
  (let [d (delay (sync-consumer results width height))]
    (doto
      (Thread. ^Runnable
               (fn []
                 @d
                 (println "Done...")))
      (.start))))

我还尝试了更新到1.9.0。我认为这可能会解决此问题,因为@Alex说他在1.9.0上,无法重现此问题,there's also this bug fix that seems related.不幸的是,我没有发现任何区别。

如果有一个切实可行的机制,这将很好。 ^:once看起来不错,但是我不想使用它只是为了以后可能损坏,而delay的使用似乎只是为了利用对象的内部{{ 1}}。

哦,至少现在可以了。谢谢大家。