Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dead simple Fork-Join concurrency in Clojure

I have two costly functions that are independent. I want to run them in parallel. I don't want to deal with futures and such (I'm new to Clojure and easily confused).

I'm looking for a simple way to run two functions concurrently. I want it to work like the following

(defn fn1 [input] ...) ; costly
(defn fn2 [input] ...) ; costly

(let [[out1 out2] (conc (fn1 x) (fn2 y))] ...)

I want this to return a vector with a pair of outputs. It should only return once both threads have terminated. Ideally conc should work for any number of inputs. I suspect this is a simple pattern.

like image 573
MRocklin Avatar asked Sep 13 '12 21:09

MRocklin


People also ask

What is the difference between concurrent execution and fork execution?

One of the concurrent executions starts at statement labeled and other execution is the continuation of the execution at the statement following the fork instruction. The fork systems call assignment has one parameter i.e. Label (L).

What is the difference between forkjoinpool recursiveaction and recursivetask?

Here a new ForkJoinPool with a parallelism level of 4 CPUs. RecursiveAction represents a task which does not return any value. RecursiveTask represents a task which returns a value. The following TestThread program shows usage of Fork-Join framework in thread based environment.

What is the fork-join framework?

The fork-join framework allows to break a certain task on several workers and then wait for the result to combine them. It leverages multi-processor machine's capacity to great extent. Following are the core concepts and objects used in fork-join framework.

What is Fork instruction in C++?

The fork instruction is the that instruction in the process execution that produces two concurrent executions in a program. One of the concurrent executions starts at statement labeled and other execution is the continuation of the execution at the statement following the fork instruction.


Video Answer


3 Answers

Using futures is very easy in Clojure. At any rate, here is an answer that avoids them

(defn conc [& fns]
  (doall (pmap (fn [f] (f)) fns)))

pmap uses futures under the hood. doall will force the sequence to evaluate.

(let [[out1 out2] (conc fn1 fn2)]
        [out1 out2])

Note, that I destructured out1 and out2 in an attempt to preserve your example.

like image 171
Julien Chastang Avatar answered Sep 28 '22 06:09

Julien Chastang


You do need a macro to preserve the desired syntax, though there are other ways of obtaining the same behavior, as the other answers indicate. Here is one way to do it:

(defn f1 [x] (Thread/sleep 500) 5)
(defn f2 [y] 2)

(defmacro conc [& exprs]
  `(map deref
        [~@(for [x# exprs] `(future ~x#))]))

(time (let [[a b] (conc (f1 6) (f2 7))]
       [a b]))
; "Elapsed time: 500.951 msecs"
;= (5 2)

The expansion shows how it works:

(macroexpand-1 '(conc (f1 6) (f2 7)))
;= (clojure.core/map clojure.core/deref [(clojure.core/future (f1 6)) 
;=                                       (clojure.core/future (f2 7))])

You specified two functions but this should work with any number of expressions.

like image 32
JohnJ Avatar answered Sep 28 '22 08:09

JohnJ


I understand you don't want your final solution to expose futures though it is useful to illustrate how to do this with futures, and then wrap them in something that hides this detail:

core> (defn fn1 [input] (java.lang.Thread/sleep 2000) (inc input))
#'core/fn1                                                                                     
core> (defn fn2 [input] (java.lang.Thread/sleep 3000) (* 2 input))
#'core/fn2                                                                                     
core> (time (let [f1 (future (fn1 4)) f2 (future (fn2 4))] @f1 @f2))
"Elapsed time: 3000.791021 msecs"  

then we can wrap that up in any of the many clojure wrappers around futures. the simplest being just a function which takes two functions and runs them in parallel.

core> (defn conc [fn1 fn2] 
         (let [f1 (future (fn1)) 
               f2 (future (fn2))] [@f1 @f2]))
#'core/conc                                                                                    
core> (time (conc #(fn1 4) #(fn2 4)))
"Elapsed time: 3001.197634 msecs"                                                                          

This avoids the need to write it as a macro by having conc take the function to run instead of the body to evaluate, and then create the functions to pass to it by putting # infront of the calls.

This can also be written with map and future-call:

core> (map deref (map future-call [#(fn1 4) #(fn2 42)]))
(5 84)  

You can then improce conc until it resembles (as Julien Chastang wisely points out) pmap

like image 38
Arthur Ulfeldt Avatar answered Sep 28 '22 07:09

Arthur Ulfeldt