Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why are only 32 threads running when calling futures in clojure?

When running a test on futures I found:

user=> (time (doall (map deref (for [i (range 1000)]
  #_=>    (future (Thread/sleep 1000))))))
"Elapsed time: 32058.208 msecs"

When powers of 2 are visible there are alarmbells ringing in my head. This smells like only 32 threads are started.

Some targetted experiments:

user=> (time (doall (map deref (for [i (range 32)]
  #_=> (future (Thread/sleep 1000))))))
"Elapsed time: 1002.302 msecs"
(nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil)
user=> (time (doall (map deref (for [i (range 64)]
  #_=> (future (Thread/sleep 1000))))))
"Elapsed time: 2004.24 msecs"
(nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil)
user=> (time (doall (map deref (for [i (range 65)]
  #_=> (future (Thread/sleep 1000))))))
"Elapsed time: 3005.279 msecs"
(nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil     nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil)

confirm this. We see an additional second for each slice of 32 future tasks.

The code to make a future is

(defmacro future
  "Takes a body of expressions and yields a future object that will
  invoke the body in another thread, and will cache the result and
  return it on all subsequent calls to deref/@. If the computation has
  not yet finished, calls to deref/@ will block, unless the variant of
  deref with timeout is used. See also - realized?."
  {:added "1.1"}
  [& body] `(future-call (^{:once true} fn* [] ~@body)))

the interesting bit in future-call is

    fut (.submit clojure.lang.Agent/soloExecutor ^Callable f)]

which translates to:

volatile public static ExecutorService soloExecutor = Executors.newCachedThreadPool(
    createThreadFactory("clojure-agent-send-off-pool-%d", sendOffThreadPoolCounter));

So this creates indeed an unbounded threadpool.

So why are only 32 threads running instead of creating 1000 threads and returning in a second?

like image 591
Peter Tillemans Avatar asked Sep 23 '13 12:09

Peter Tillemans


1 Answers

This

(time (doall (map deref (for [i (range 65)] (future (Thread/sleep 1000))))))
; "Elapsed time: 3005.244983 msecs"

should actually take 65 seconds because you are asking to deref the futures in sequence on the main thread. These futures have not even started when the deref comes because for is lazy. The reason you get multiples of 32 is the chunking behavior of range.

Compare to non-chunked version

 (time (doall (map deref (for [i (apply list (range 65))] (future (Thread/sleep 1000))))))
; "Elapsed time: 64997.260808 msecs"

As pointed out by Rörd in the comments, adding another doall to remove the laziness of the future creation solves the problem.

(time (doall (map deref (doall (for [i (range 65)] (future (Thread/sleep 1000)))))))
; "Elapsed time: 999.263631 msecs"

Another way to test completion of all the futures is to wait on a promise that is not delivered until the last future completes.

(defn test-futures [n]
  (let [a (atom 0)
        p (promise)
        f (fn [] (swap! a inc) (when (= @a n) (deliver p n)))]
    (doseq [i (range n)] (future (do (Thread/sleep 1000) (f))))
    (deref p))) 

And now you can see that completion of 64, 65, or 1000 of these futures occurs each in about 1 second.

(time (test-futures 64))
; "Elapsed time: 996.262272 msecs"
; 64
(time (test-futures 65))
; "Elapsed time: 996.554436 msecs"
; 65
(time (test-futures 1000))
; "Elapsed time: 1000.247374 msecs"
; 1000
like image 189
A. Webb Avatar answered Nov 16 '22 03:11

A. Webb