如何在Clojure中实现并行逻辑或提前终止

时间:2019-04-30 16:27:31

标签: clojure parallel-processing functional-programming logic terminate

我想定义一个谓词,将某些谓词作为输入 带有相应的输入(它们可以作为懒惰的调用序列给出), 并行运行它们并计算逻辑或结果, 以这样的方式,在谓词调用终止返回true的那一刻, 整个计算也将终止(返回true)。

除了提供时间优化外,这还有助于避免 在某些情况下不终止(某些谓词调用可能不会终止)。 实际上,将非终止解释为第三个undefined值, 该谓词模拟Kleene's K3 logic中的or操作 (在初始居中的Kleene algebra中加入)。

Haskell家族here也有类似的报道。 Clojure中有什么方法(最好是简单的方法)吗?

编辑:在阅读评论后,我决定添加一些说明。

(a)首先,在线程池用尽之后发生的事情不太重要。我认为创建一个足以满足我们需求的线程池是一种合理的约定。

(b)最关键的要求是谓词调用开始并行运行,并且一旦谓词调用终止返回true,所有其他运行线程都会被中断。预期的行为是:

  • 如果有谓词调用返回true:并行或返回true
  • 如果存在不终止的谓词调用:并行或不终止
  • else:并行或返回false

换句话说,它的行为类似于false <undefined <true给定的3元素晶格中的联接,其中undefined表示非终止。 / p>

(c)并行或应该能够接受许多谓词和许多谓词输入(每个对应于一个谓词)作为输入。但是,如果将延迟序列作为输入,那就更好了。然后,命名并行或pany(对于“并行任何”而言),我们可以进行如下调用:

  • (pany (map (comp eval list) predicates inputs))
  • (pany (map (comp eval list) predicates (repeat input)))
  • (pany (map (comp eval list) (repeat predicate) inputs))等效于(pany (map predicate (unchunk inputs)))

最后一点,我认为要求pany,双重pall之类的东西或建立这样的尽早终止并行约简的机制很容易实现甚至什至很自然内置于诸如Clojure这样的面向并行的语言中。

2 个答案:

答案 0 :(得分:0)

我将根据约简函数定义谓词。实际上,我们可以重新实现所有Clojure迭代函数以支持此并行操作,但我仅以reduce为例。

我将定义一个计算函数。我只会使用相同的,但不会阻止您拥有很多。如果累计1000,则该函数为“ true”。

(defn computor [acc val]
        (let [new (+' acc val)] (if (> new 1000) (reduced new) new)))

(reduce computor 0 (range))
;; =>
1035

(reduce computor 0 (range Long/MIN_VALUE 0))
;; =>
;; ...this is a proxy for a non-returning computation

;; wrap these up in a form suitable for application of reduction
(def predicates [[computor 0 (range)] 
                 [computor 0 (range Long/MIN_VALUE 0)]])

现在让我们开始讨论。我想在每次计算中都迈出一步,如果其中一项计算完成,我想将其返回。实际上,一次使用pmap一步很慢-工作单元太小,不值得进行线程化。在这里,我已经进行了一些更改,以便继续进行每个工作单元的1000次迭代。您可能会根据工作量和步骤成本进行调整。

(defn p-or-reducer* [reductions]
        (let [splits (map #(split-at 1000 %) reductions) ;; do at least 1000 iterations per cycle
              complete (some #(if (empty? (second %)) (last (first %))) splits)]
          (or complete (recur (map second splits)))))

然后将其包装在驱动程序中。

(defn p-or [s]
  (p-or-reducer* (map #(apply reductions %) s)))

(p-or predicates)
;; =>
1035

在哪里插入CPU并行性? p-or-reducer *中的s / map / pmap /应该这样做。我建议仅并行化第一个操作,因为这将驱动归约序列进行计算。

(defn p-or-reducer* [reductions]
        (let [splits (pmap #(split-at 1000 %) reductions) ;; do at least 1000 iterations per cycle
              complete (some #(if (empty? (second %)) (last (first %))) splits)]
          (or complete (recur (map second splits)))))

(def parallelism-tester (conj (vec (repeat 40000 [computor 0 (range Long/MIN_VALUE 0)]))
                             [computor 0 (range)]))

(p-or parallelism-tester) ;; terminates even though the first 40K predicates will not

定义一个高性能的通用版本非常困难。在不知道每次迭代的成本的情况下,很难得出有效的并行化策略-如果一次迭代需要10秒钟,那么我们可能一次只迈出一步。如果要花费100ns,那么我们需要一次采取许多步骤。

答案 1 :(得分:0)

您是否考虑采用core.async来处理async/goasync/thread的并行任务,并使用async/alts!尽早返回?

例如,将核心or功能从串行转换为并行。我们可以创建一个宏(我称其为por)来将输入函数(或谓词)包装到async/thread中,然后在它们之上进行套接字选择async/alts!

(defmacro por [& fns]
  `(let [[v# c#] (async/alts!!
                  [~@(for [f fns]
                       (list `async/thread f))])]
     v#))

(time
 (por (do (println "running a") (Thread/sleep 30) :a)
      (do (println "running b") (Thread/sleep 20) :b)
      (do (println "running c") (Thread/sleep 10) :c)))
;; running a
;; running b
;; running c
;; "Elapsed time: 11.919169 msecs"
;; => :c

与原始or(串行运行)相比:

(time
 (or (do (println "running a") (Thread/sleep 30) :a)
     (do (println "running b") (Thread/sleep 20) :b)
     (do (println "running c") (Thread/sleep 10) :c)))
;; running a
;; => :a
;; "Elapsed time: 31.642506 msecs"