如何记忆使用core.async和非阻塞通道读取的函数?

时间:2014-07-11 13:08:07

标签: clojure clojurescript core.async

我想将memoize用于使用core.async<!的函数,例如

(defn foo [x]
  (go
    (<! (timeout 2000))
    (* 2 x)))

(在现实生活中,它可能有助于缓存服务器调用的结果)

我能够通过编写memoize的core.async版本来实现这一点(几乎与memoize相同的代码):

(defn memoize-async [f]
  (let [mem (atom {})]
    (fn [& args]
      (go
        (if-let [e (find @mem args)]
          (val e)
         (let [ret (<! (apply f args))]; this line differs from memoize [ret (apply f args)]
            (swap! mem assoc args ret)
            ret))))))

使用示例:

(def foo-memo (memoize-async foo))
(go (println (<! (foo-memo 3)))); delay because of (<! (timeout 2000))

(go (println (<! (foo-memo 3)))); subsequent calls are memoized => no delay

我想知道是否有更简单的方法可以达到相同的效果。

**备注:我需要一个适用于<!的解决方案。对于<!!,请参阅此问题:How to memoize a function that uses core.async and blocking channel read? **

3 个答案:

答案 0 :(得分:1)

您可以使用内置的memoize功能。首先定义一个从通道读取并返回值的方法:

 (defn wait-for [ch]
      (<!! ch))

请注意,我们会使用<!!而不是<!,因为我们需要此功能块,直到所有情况下都有通道上的数据。 <!仅在go块内的表单中使用时才会出现此行为。

然后,您可以通过将此函数与foo组合来构建您的memoized函数,如下所示:

(def foo-memo (memoize (comp wait-for foo)))

foo会返回一个频道,因此wait-for会阻止,直到该频道有一个值(即直到foo内的操作完成)。

foo-memo可以与您上面的示例类似,但您不需要调用<!,因为wait-for会阻止您:

(go (println (foo-memo 3))

你也可以在go块之外调用它,它会表现得像你期望的那样(即阻止调用线程直到foo返回)。

答案 1 :(得分:0)

这比我想象的要复杂一点。你的解决方案是不正确的,因为当你用相同的参数再次调用你的memoized函数时,比第一次运行完成运行它的go块更快,你将再次触发它并得到一个未命中。使用core.async处理列表时通常会出现这种情况。

下面的一个使用core.async的pub / sub来解决这个问题(仅在CLJS中测试):

(def lookup-sentinel  #?(:clj ::not-found :cljs (js-obj))
(def pending-sentinel #?(:clj ::pending   :cljs (js-obj))

(defn memoize-async
  [f]
  (let [>in (chan)
        pending (pub >in :args)
        mem (atom {})]
    (letfn
        [(memoized [& args]
           (go
             (let [v (get @mem args lookup-sentinel)]
               (condp identical? v
                 lookup-sentinel
                 (do
                   (swap! mem assoc args pending-sentinel)
                   (go
                     (let [ret (<! (apply f args))]
                       (swap! mem assoc args ret)
                       (put! >in {:args args :ret ret})))
                   (<! (apply memoized args)))
                 pending-sentinel
                 (let [<out (chan 1)]
                   (sub pending args <out)
                   (:ret (<! <out)))
                 v))))]
        memoized)))

注意:它可能会泄漏内存,订阅和<out频道未关闭

答案 2 :(得分:0)

我在一个项目中使用了此功能来缓存HTTP调用。该函数在给定的时间内缓存结果,并使用屏障来防止缓存“冷”时多次执行该函数(由于go块内的上下文切换)。

(defn memoize-af-until
  [af ms clock]
  (let [barrier (async/chan 1)
        last-return (volatile! nil)
        last-return-ms (volatile! nil)]
    (fn [& args]
      (async/go
        (>! barrier :token)
        (let [now-ms (.now clock)]
          (when (or (not @last-return-ms) (< @last-return-ms (- now-ms ms)))
            (vreset! last-return (<! (apply af args)))
            (vreset! last-return-ms now-ms))
          (<! barrier)
          @last-return)))))

您可以通过将缓存时间设置为0来测试它是否正常工作,并观察两个函数调用大约花费10秒。没有障碍,两个呼叫将同时完成:

(def memo (memoize-af-until #(async/timeout 5000) 0 js/Date))
(async/take! (memo) #(println "[:a] Finished"))
(async/take! (memo) #(println "[:b] Finished"))