Мертвый простой параллелизм 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