Concurrency Clojure中的死简单Fork-Join并发

Concurrency Clojure中的死简单Fork-Join并发,concurrency,clojure,future,Concurrency,Clojure,Future,我有两个独立的昂贵功能。我想让它们并行运行。我不想处理期货之类的事情(我是Clojure的新手,很容易被弄糊涂) 我正在寻找一种同时运行两个函数的简单方法。我希望它能像下面那样工作 (defn fn1 [input] ...) ; costly (defn fn2 [input] ...) ; costly (let [[out1 out2] (conc (fn1 x) (fn2 y))] ...) 我想让它返回一个带有一对输出的向量。它应该只在两个线程终止后返回。理想情况下,conc应适用

我有两个独立的昂贵功能。我想让它们并行运行。我不想处理期货之类的事情(我是Clojure的新手,很容易被弄糊涂)

我正在寻找一种同时运行两个函数的简单方法。我希望它能像下面那样工作

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

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

我想让它返回一个带有一对输出的向量。它应该只在两个线程终止后返回。理想情况下,conc应适用于任意数量的输入。我怀疑这是一个简单的模式。

在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,直到它类似于(Julien Chastang明智地指出)
pmap
您确实需要一个宏来保留所需的语法,尽管有其他方法可以获得相同的行为,如其他答案所示。以下是一种方法:

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

您指定了两个函数,但这应该适用于任意数量的表达式。

当您说不想处理futures时,是否意味着您也不想在“conc”函数中使用futures?据我所知,在这种情况下使用Clojure并发原语是惯用的,尽管它们可以通过“conc”中的封装对您隐藏。肯定会使用一些并发原语。conc可以像你喜欢的那样复杂。我只是不想把他们当作用户来处理。我怀疑这是“为每个输入开始未来”,“等待每个输出”,“返回”。也许它必须是一个宏,不确定。如果你想把参数的计算推迟到conc线程,那么肯定是一个宏。我现在正在处理宏def'n。要使用它,您需要将输入封装在匿名函数中,即:#(fn1 42)@arthurlfeldt correct,在这种情况下,我将使用JohnJ的解决方案。我认为不将其封装在宏中会产生更好的可组合代码。宏不是一流的东西,您不能将它们传递到映射或应用到参数列表。太棒了。这激励我自己去尝试。我试图用
(映射未来表达式)
替换for。可悲的是,这并没有起作用,因为未来本身就是一个宏。有没有想过如何进一步减少这个问题?是的,这就是问题所在,宏不能像函数那样轻松地组合它们。我不知道如何进一步减少,但有人可能会这样做。