如何在不使用clojurescript中的事件循环的情况下运行长计算?

时间:2014-10-04 20:33:04

标签: javascript clojurescript event-loop

一个简单(但很慢)的功能

我想利用clojurescript在浏览器中进行一些声明性的,功能性的编程 - 例如,能够进行懒惰的计算,如:

(def special-nums 
  (->> (iterate inc 1)
       (filter #(= (mod % 100) 0))
       (filter #(= (mod % 7001) 0))))

(time (doall (take 1 special-nums)))

(请注意,这个特定的计算对我来说并不是很有趣 - 它只是一些代码的例子,即使是第一个结果也会花费不明的时间返回)

以块的形式绑定CPU时间的任何方法吗?

这样的代码对于clojure来说很自然,但是它不适合在浏览器环境中调用,它可能会阻塞事件循环并使网页无响应。懒惰没有帮助,因为即使是第一个结果也可能需要很长时间才能返回(我机器上的时间为1500毫秒)。

在“普通”javascript中,使用命令式循环,我会将范围缩小并使用setTimeout异步返回结果,从而限制我愿意在任何给定块中执行的工作。 (我甚至可以明确地引用时钟 - 例如“继续工作直到20ms已经过去,然后停止并安排另一个块。”

有没有一种方法可以在clojurescript中实现这一目标?显然任何事情都可以通过js互操作,但如果我对系统的斗争太过激烈,那么cljs的价值就会受到限制。

我很欣赏这里的任何建议/技巧。

注意

我知道网络工作者,我知道将计算推送到另一个执行线程总是可行的 - 但对于这个问题,我想专注于在单个JS事件循环中工作的方法。

谢谢!

2 个答案:

答案 0 :(得分:2)

启发这个答案的技术是trampoline - 一个返回答案的函数,或另一个返回答案或函数的函数.....(在这里插入递归的英文文本)。< / p>

我们实际上不能在这里使用clojure.core/trampoline因为它会占用JS消息循环。相反,我们可以使用类似下面的内容,这些内容会在js/setTimeout内“反弹”。当f返回某个函数时,我们会从setTimeout调用它。当f返回任何其他内容时,我们会调用(continuation result)

(defn continue-trampoline [continuation f & args]
  (let [result-or-fn (apply f args)
        is-answer (not (fn? result-or-fn))]
    (if is-answer (continuation result-or-fn)
      (do (js/setTimeout #(continue-trampoline continuation result-or-fn) 1)
          nil))))

这使我们可以将问题划分为更小的部分,可以在更短的时间内单独解决。

以你的特殊例子为例,你可以像这样分解它:

(defn calculate-special-nums [n continuation]
  (letfn [(accumulate-special-nums [accumulator partitions]
           (if (empty? partitions) accumulator
             (let [part (first partitions)
                   remaining-n (- n (count accumulator))
                   acc (->> part
                            (filter #(= (mod % 100) 0))
                            (filter #(= (mod % 7001) 0))
                            (take remaining-n)
                            (into accumulator))
                   is-complete (== n (count acc))]
               (if is-complete acc
                 #(accumulate-special-nums acc (rest partitions))))))]
    (let [nums (iterate inc 1)
          partitions (partition 1000 nums)]
      (continue-trampoline continuation
                           #(accumulate-special-nums [] partitions)))))

因此,此代码将计算10个特殊数字,并在计算所有10个时提醒它们,而不会使消息循环的其他用户挨饿。

(calculate-special-nums 10 #(js/alert %))

该技术可能会扩展到占用毫秒数。对于您的示例,我可以想象使用partition-by而不是partition。创建一个经过一段时间后返回true的函数。例如(partition-by has-the-time-elapsed? nums)代替(partition 1000 nums)

其他问题

正如你所说,即使在“正常”的javascript中,你也必须解决问题 - clojurescript可能会或可能不会成为昂贵计算的例外。使用纯函数范例进行编程的一个好处是每个部分都是可独立测试的。每个不同的输入的输出始终相同。希望clojurescript能够使分区问题变得更容易。

答案 1 :(得分:0)

在探索@ agent-j优秀的基于蹦床的方法后,我决定尝试core.async方法进行比较。

它运行得非常好,它允许我用find-special-numbers计算器将现有的by-chunk函数包装成具有相当声明的语法:

(ns calculate.core
  (:require
    [figwheel.client :as fw]
    [cljs.core.async :refer [chan put! <! ]])
  (:require-macros [cljs.core.async.macros :refer [go-loop]]))

(enable-console-print!)

; this is the slow, dumb function -- designed to
; take a limited-size input and produce 0 or more
; results
(defn find-special-numbers [input]
  (->> input
       (filter #(= (mod % 100) 0))
       (filter #(= (mod % 7001) 0))))

(defn now [] (-> (js/Date.) .getTime))

(defn time-slice-in-ms [ms]
  (let [start (now)]
    (fn [] (-> (now) (- start) (quot ms)))))

; run a single chunk, report results, and schedule
; the next chunk of work in the event loop.
(defn by-chunk-step [chunks calc-fn result-chan]
  (doseq [r (calc-fn (first chunks))]
    (put! result-chan r))
  (js/setTimeout #(by-chunk-step (rest chunks) calc-fn result-chan)))

; transform a set of inputs into chunks, then run
; kick off job to run a calculation function on each chunk.
; meanwhile, immediately return a result-reporting channel.
(defn by-chunk [inputs [ms] calc-fn]
  (let [result-chan (chan)
        chunks (partition-by (time-slice-in-ms ms) inputs)]
    (by-chunk-step chunks calc-fn result-chan)
    result-chan))

(defn all-ints [] #(iterate inc 1))

(defn load []
  (let [results (by-chunk (all-ints) [20 :ms] find-special-numbers)]
    (go-loop []
             (println (<! results))
             (recur))))

(fw/watch-and-reload :jsload-callback (load))

注意定义all-ints时有一个问题:重要的是不要保留此列表的头部,否则堆栈使用量会不受限制地增长,浏览器将崩溃。因此all-ints直接返回函数而不是惰性列表。