【问题标题】:Short-circuiting futures in clojureclojure 中的短路期货
【发布时间】:2013-06-01 01:43:27
【问题描述】:

我有两个解析为布尔值的期货。我基本上想做类似的事情

(if (or @future1 @future2) 
  ...)

但是,无论哪个先完成的优化,如果它是真的,那么我不会等待剩余的未来完成;去吧。当然,如果值为 false,则等待剩余的未来完成。有没有一种简单的方法可以做到这一点?

【问题讨论】:

  • 达克斯,期货是否“返回”价值?不,因为期货不是函数。期货(又名延迟或承诺)是用值解析的,这在概念上与返回值的函数非常不同。考虑一下差异,您的问题应该会变得更加清晰。
  • @Beetroot-Beetroot 是的,我明白这一点。虽然这个问题没有答案,但我想知道答案是否真的那么清楚。你有什么想法吗?我有一些想法,但大多数都非常复杂,我希望对看似简单的问题有一个简单的答案。我用更准确的措辞更新了问题。
  • Dax,虽然我一般理解期货,但我不懂这种特殊的语言。我认为@AlexTaggart 的答案接近您想要的,但您需要获得这两个承诺(可能是通过函数调用)而不是在foo 中生成它们。
  • 如果你会阅读javascript,那么我可以原则上告诉你如何做到这一点。您需要翻译回 clojure。

标签: asynchronous clojure future promise


【解决方案1】:

在一般情况下,您可以对两个交付者做出相同的承诺。例如:

(defn foo []
  (let [p (promise)]
    (future
      (Thread/sleep (rand-int 1000))
      (deliver p :a))
    (future 
      (Thread/sleep (rand-int 1000))
      (deliver p :b))
    @p))

一旦第一个deliver 出现,调用(foo) 将随机产生:a:b;另一个 deliver 将是空操作。

对于您的具体情况,您需要返回两个布尔值。我唯一能想到的(而且有点混乱)是使用交付者之间共享的第三个承诺:

(defn foo []
  (let [a (promise)
        b (promise)
        link (promise)]
    (future
      (Thread/sleep (rand-int 5000))
      (let [res (rand-nth [true false])]
        (deliver link res)
        (deliver a res)))
    (future
      (Thread/sleep (rand-int 5000))
      (let [res (rand-nth [true false])]
        (deliver link res)
        (deliver b res)))
    {:or (or @link @a @b)  
     :a? (realized? a) 
     :b? (realized? b)
     :link @link
     :a @a 
     :b @b}))
  • 如果a 先传递trueor 会立即完成。
  • 如果a 先传递false@a 立即返回,然后阻塞@b
  • 如果b 先传递trueor 会立即完成。
  • 如果a 先发送false,它会阻塞@a

重复调用(foo),您应该会看到预期的结果,具体来说,当:ortrue 时,有时:a?:b? 将是false,但如果两者都将始终为true :orfalse

【讨论】:

  • 感谢您的努力,但我认为这两种方法都不适合我的情况——我无法访问调用deliver 的代码。我更新了问题以使其更加明显。
  • futures 的常见情况是你想要所有的数据,但可以并行完成工作。短路与此不同。我倾向于说只使用or。最短的执行时间是第一个 future 在第二个 future 完成之前产生 true,最长的执行是第一个 future 产生 false,第二个需要更多时间才能完成。
【解决方案2】:

现在可以使用core.async,正如我对新with Clojure threading long running processes and comparing their returns 问题的回答中所述。该答案定义了thread-and;这个问题需要thread-or

(defn thread-or
  "Call each of the fs on a separate thread. Return logical
  disjunction of the results. Short-circuit (and cancel the calls to
  remaining fs) on first truthy value returned."
  [& fs]
  (let [futs-and-cs
        (doall (for [f fs]
                 (let [c (chan)]
                   [(future (>!! c (f))) c])))]
    (loop [futs-and-cs futs-and-cs]
      (let [[result c] (alts!! (map peek futs-and-cs))]
        (if result
          (do (doseq [fut (map first futs-and-cs)]
                (future-cancel fut))
              result)
          (let [new-futs-and-cs (remove #(identical? (peek %) c)
                                        futs-and-cs)]
            (if (next new-futs-and-cs)
              (recur new-futs-and-cs)
              (<!! (peek (first new-futs-and-cs))))))))))

用(常为假)和(常为真)测试:

(thread-or (constantly true) (constantly true))
;;= true

(thread-or (constantly true) (constantly false))
;;= true

(thread-or (constantly false) (constantly false))
;;= false

另请注意,短路确实有效:

;; prints :foo before returning true
(thread-or #(do (Thread/sleep 3000) true)
           #(do (Thread/sleep 1000) (println :foo)))

;; does not print :foo
(thread-or #(do (Thread/sleep 3000) true)
           #(do (Thread/sleep 7000) (println :foo)))

【讨论】:

  • 是否有可能与 core.match 一起使用,从而执行第一组匹配结果的谓词?
  • 您当然可以将 core.match 与从通道返回的值一起使用,毕竟它们只是常规的 Clojure 值。在thread-or 变体中,第一个真值短路,您可以简单地返回该值,然后在其上使用 core.match。如果您想做一个thread-and,然后最后使用 core.match 计算第一个值,您必须将第一个返回值存储在本地循环中(将其添加为第二个循环本地 -- res1,比如说 -- 初始值nil,然后(recur ... (if (nil? res1) result res1))... 和以前一样)。
  • 当然,在真实情况下,您必须从thread-and 返回。 (除非您想将匹配连接到 thread-and;或者您可以重写它,以便您可以从外部将该逻辑作为函数传递。)
  • 其实,给我一分钟,我会发帖thread-or;实际上我本来打算这样做,但由于某种原因最终粘贴在thread-and
  • 好的,这里是thread-or。也可在此gist 中找到。
【解决方案3】:

我非常喜欢这个问题。也许我很惊讶看到一个如此简单的新问题。无论如何,流口水,我认为我有一些适合你的东西。

而不是做:

(if (or @future1 @future2)
  ...)

做:

(if (or (and (realized? future1) @future1)
        (and (realized? future2) @future2))
  ...)

诀窍是在询问是true 还是false 之前测试是否已实现。

这可以概括为这样的宏:

(defmacro future-or [& futures]
  `(or ~@(for [f futures]
          `(and (realized? ~f)
                (deref ~f)))))

然后像这样使用:

(if (future-or future1 future2)
  ...)

等一下。也许我不正确地理解你的问题。也许您想阻止执行,直到其中一个期货完成并返回 true,在这种情况下,您执行 if 的 then 子句,或者您的两个期货都完成并且都不返回 true,其中如果您执行 if 的 else 子句。那是另一回事。

我能想出的最简洁的方法不是很漂亮,但也不是很长:

(if (loop []
        (cond (or (and (realized? future1) @future1)
                  (and (realized? future2) @future2)) true
              (and (realized? future1) (realized? future2)
                   (not @future1) (not @future2)) false
              :else (recur)))
    ...)

现在,这使用loop 重复循环,直到发生以下两种情况之一:任一期货都已实现和true,在这种情况下,循环返回true;或者所有期货都已实现并且全部为false,在这种情况下,循环返回false。将 (not ...) 表达式放在其父 (and ...) 表达式的末尾很重要,这样您就不会在检查任何期货是 truefalse 之前遇到困难,直到它们全部实现。

这个新的解决方案可以概括为:

(defmacro future-or [& futures]
  `(loop []
     (cond (or ~@(for [f futures]
                   `(and (realized? ~f)
                         (deref ~f)))) true
           (and ~@(for [f futures]
                    `(realized? ~f))
                ~@(for [f futures]
                    `(not (deref ~f)))) false
           :else (recur))))

并且使用方式和上面future-or的例子一样。

现在,我对优化几乎一无所知。但据我所知,这肯定没有理论上的效率,因为一旦实现了任何给定的未来,就没有必要多次测试它的价值。好吧,这里有两个解决方案,我将其命名为 future-some。由于被测试的期货必须在每次循环迭代后动态变化,我不得不将它变成一个函数。这样,这种新方法类似于some,而不是or。实际上,我改变了行为以获取期货集合(而不是可变数量的单个参数——someor 之间的另一个区别)。一种解决方案不进行双重检查(我认为):

(defn future-some [futures]
  (if (empty? futures)
    false
    (let [res (reduce (fn [unrealized f]
                        (if (realized? f)
                          (if @f
                            (reduced true)
                            unrealized)
                          (cons f unrealized)))
                      ()
                      futures)]
      (if (true? res)
        true
        (recur res)))))

这里有很多细节需要说明,但要点是:如果没有要测试的期货,则返回false,否则,它会向下迭代期货列表,测试它们是否已实现。如果实现了未来,并且还取消了对true 的引用,则迭代中断并返回true。如果实现了未来但没有取消引用true,则迭代继续到下一项。如果未来未实现,则添加一个列表以用于future-some 的下一次递归。

另一种解决方案更简洁,但不太理想:

(defn future-some [futures]
  (if (empty? futures)
    false
    (let [realized (filter realized? futures)]
      (if (some deref realized)
        true
        (recur (remove (set realized) futures))))))

与另一个类似,除了它首先过滤掉已实现,然后测试,然后再次过滤(这次是反向 - 以获得未实现的),如果它需要重复。这第二个过滤是低效的步骤。

我提出的所有解决方案的一个问题是,未来的取消会在取消引用时导致错误,而他们可能应该简单地继续下去,就好像未来是错误的一样。这可以通过在任何取消引用之前将 (not (future-cancelled? ...)) 表达式放在每个 (and ...) 表达式中来解决。或者,对于future-some 函数,您必须将realized? 谓词替换为(some-fn (comp not future-cancelled?) realized?),或者为胆小的人替换#(and (not (future-cancelled %)) (realized? %))

再次,说真的,感谢您提出这个问题。

【讨论】:

  • 只是为了对思考过程有所贡献,阅读此内容后问题来了:tomasp.net/blog/fsharp-variations-joinads.aspx。他们必须创建一个特殊的编译器扩展来启用它,但最终它不仅允许上述内容,而且还允许对异步位进行完全模式匹配。我认为 Clojure 是一种语言,它可以为问题提供极其优雅的解决方案。
  • @DaxFohl 感谢您的链接。也许有一天,如果/当我对 F# 和/或 monad 有所了解时,我会更加了解它。
【解决方案4】:

假设您不想每 X 毫秒轮询一次期货,并且您无法控制期货/线程的创建或它们正在执行的 fn,那么解决方案就是创建更多线程,每个线程都在等待为了未来:

(defn wait-for-any [& futures]
  (let [how-many-left (atom (count futures))
        p (promise)
        wait-and-notify (fn [f]
                         (fn []
                           (if @f
                             (deliver p true)
                             (when (zero? (swap! how-many-left dec))
                               (deliver p false)))))]
    (dorun (map (comp future-call wait-and-notify) futures))
    @p))

 (defn sleep-and-return [what-to-return sleep-time]
  (future
    (Thread/sleep sleep-time)
    what-to-return))


(time (println (wait-for-any
                (sleep-and-return false 1000)
                (sleep-and-return false 2000)
                (sleep-and-return false 3000)
                (sleep-and-return false 4000)
                (sleep-and-return false 5000))))

>>>false
>>>"Elapsed time: 5000.933906 msecs"

(time (println (wait-for-any
                (sleep-and-return false 1000)
                (sleep-and-return true 2900)
                (sleep-and-return true 3000)
                (sleep-and-return false 4000)
                (sleep-and-return false 5000))))
>>>true
>>>"Elapsed time: 2901.309695 msecs"

【讨论】:

    猜你喜欢
    • 2012-02-10
    • 2012-01-01
    • 2011-01-26
    • 1970-01-01
    • 1970-01-01
    • 2017-08-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多