如何并行制作阻止调用列表并以先到先得的方式获得结果?

时间:2014-11-22 15:06:08

标签: concurrency clojure parallel-processing

基本上我有一个要调用的函数列表

'(f1 f2 f3 f4)

每个都是封锁的,需要不同的时间才能返回。

我想有一个懒惰的序列s,其中第一个元素是最快返回的调用的结果,调用

(first s)

只会在该呼叫的响应时间内阻止。同样适用于其他元素。

具体示例:如果

  • f1需要10秒
  • f2需要5s
  • f3需要1s
  • f4需要2s

然后调用

(first s)

将阻止1并返回(f3)

的结果
(into [] (take 2 s)) 

将阻止2s并返回(f3)和(f4)等的结果

我已经考虑过将期货中的所有功能包装起来并将结果交给承诺。 但我不知道如何理清哪个承诺将以最快的速度交付。

有没有人知道如何做到这一点?

4 个答案:

答案 0 :(得分:3)

(require '[clojure.core.async
           :refer [chan
                   >!
                   go
                   <!!]])

(def c (chan))

(def fns [#(do (Thread/sleep 5000) :fn-1)
          #(do (Thread/sleep 2000) :fn-2)
          #(do (Thread/sleep 1000) :fn-3)])
(do
  (go
    (doseq [f fns]
      (go (>! c (f)))))

  (println "First => " (<!! c)))

答案 1 :(得分:3)

使用承诺,期货和单个原子的纯粹Clojure解决方案绝对是可能的:

(defn parallelize
  [fs]
  (let [[h & r :as ps] (repeatedly (count fs) promise)
        queue (atom (cycle ps))]
    (doseq [f fs]
      (future
        (let [result (f)]
          (-> (swap! queue rest)
              (first)
              (deliver result)))))
    (map deref (concat r [h]))))

这基本上创建了一系列promise并使用atom将它们存储为循环队列。然后每个未来都会轮换队列,选择下一个可用的promise并传递函数结果。

示例:

(defn g
  [ms]
  (fn []
    (Thread/sleep ms)
    ms))

(doseq [value (parallelize (map g [500 200 100 300]))]
  (prn value))
;; 100
;; 200
;; 300
;; 500

答案 2 :(得分:1)

如果您不想使用core.async,则可以回退到一个简单的队列:

(import 'java.util.concurrent.LinkedBlockingQueue)

(defn fut [q f] ;; this will need some error handling
  (future
    (.add q (f))))

(defn take-blocking [q n]
  (when (pos? n)
    (lazy-seq
     (cons (.take q)
           (take-blocking q (dec n))))))

(defn in-parallel [fns]
  (let [queue (LinkedBlockingQueue. (count fns))]
    (doseq [f fns]
      (fut queue f))
    (take-blocking queue (count fns))))

使用它:

(defn slow [n]
  (fn []
    (Thread/sleep (* n 1000))
    n))

(doseq [r (in-parallel [(slow 5) (slow 9) (slow 1) (slow 3)])]
  (println (java.util.Date.) r))

答案 3 :(得分:1)

简单并发和控制工作线程的另一个好方法是[com.climate/claypoole "0.3.3"]。它模仿mapfor,但是并行,无论是有序还是无序,并且可以控制线程池大小(与pmap不同,其中线程池大小固定为(* 2个核心))。

以下是upmap的示例,它们是无序的并行版本的map。这意味着首先返回映射序列的最快实现版本。第一个参数是预定义的线程池,或者要使用的线程池的大小。

(require '[com.climate.claypoole :as cp]))

(defn wait-and-return
  [w]
  (Thread/sleep (* 1000 w))
  w)

(cp/upmap 4 wait-and-return [10 5 7 9])
=> (5 7 9 10)

确保将线程池的大小调整到足以容纳最大并行等待/自定义函数的数量。

(def to-sort
  (shuffle (range 0 40 2))

;not enough threads, so not returned in the right order
(def timesorted
  (time (doall (cp/upmap 10 wait-and-return to-sort))))
"Elapsed time: 52001. 812056 msecs"

(apply < timesorted)
=> false

;enough threads
(def timesorted
  (time (doall (cp/upmap 20 wait-and-return to-sort))))
"Elapsed time: 38002.858901 msecs"

(apply < timesorted)
=> true

期货不会遇到这些情况,因为它们的线程池会自动增加到最大值Integer / MAX_VALUE。但是,如果不是使用claypoole线程池或线程池大小来指定:builtin键,那么claypoole将使用Clojure自己的几乎无限制的线程池用于期货和代理发送。

但请注意,如果您不知道线程数量会增长多少,管理和切换所有这些线程会导致性能降低,因此您应该只在IO绑定情况下使用它,而不是在CPU中使用它绑定的。

(def timesorted
  (time (doall (cp/upmap :builtin wait-and-return to-sort))))
"Elapsed time: 38001.348402 msecs"

(apply < timesorted)
=> true