Мертвый простой параллелизм Fork-Join в Clojure

У меня есть две дорогостоящие функции, которые независимы. Я хочу запустить их параллельно. Я не хочу иметь дело с фьючерсами и тому подобным (я новичок в Clojure и легко запутываюсь).

Я ищу простой способ запуска двух функций одновременно. Я хочу, чтобы он работал следующим образом

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

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

Я хочу, чтобы это вернуть вектор с парой выходов. Он должен возвращаться только после завершения обоих потоков. В идеале все должно работать для любого количества входов. Я подозреваю это простая схема.

3 ответов


вам нужен макрос для сохранения желаемого синтаксиса, хотя есть и другие способы получения того же поведения, что и другие ответы. Вот один из способов сделать это:

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

расширение показывает, как это работает:

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

Вы указали две функции, но это должно работать с любым количеством выражений.


использование фьючерсов очень легко в Clojure. Во всяком случае, вот ответ, который их избегает

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

pmap использует фьючерсы под капотом. doall заставит последовательность оценить.

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

обратите внимание, что я разрушен out1 и out2 в попытке сохранить ваш пример.


Я понимаю, что вы не хотите, чтобы ваше окончательное решение выставляло фьючерсы, хотя полезно проиллюстрировать, как это сделать с фьючерсами, а затем обернуть их чем-то, что скрывает эту деталь:

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"  

тогда мы можем завернуть это в любую из многих оберток clojure вокруг фьючерсов. самое простое - это просто функция, которая принимает две функции и запускает их параллельно.

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"                                                                          

это позволяет избежать необходимости писать его как макрос, имея conc взять функцию для запуска вместо тела оценить, а затем создать функции, чтобы перейти к нему, поставив # перед звонков.

Это также может быть написано с помощью map и future-call:

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

затем вы можете импровизировать conc, пока он не будет похож (как мудро указывает Жюльен Шастанг)pmap