【问题标题】:How does one start a thread in Clojure?如何在 Clojure 中启动一个线程?
【发布时间】:2010-12-18 14:55:00
【问题描述】:

我读过很多关于 Clojure 在并发方面有多棒的文章,但我读过的教程都没有真正解释如何创建线程。你只是做(.start(Thread.func)),还是我错过了另一种方式?

【问题讨论】:

    标签: multithreading concurrency clojure


    【解决方案1】:

    Clojure fns 是 Runnable,因此通常按照您发布的方式使用它们,是的。

    user=> (dotimes [i 10] (.start (Thread. (fn [] (println i)))))
    0                                                             
    1                                                             
    2                                                             
    4                                                             
    5                                                             
    3                                                             
    6                                                             
    7                                                             
    8                                                             
    9                                                             
    nil
    

    另一种选择是使用agents,在这种情况下,您将使用sendsend-off,它会使用池中的线程。

    user=> (def a (agent 0))
    #'user/a
    user=> (dotimes [_ 10] (send a inc))
    nil
    ;; ...later...
    user=> @a
    10
    

    另一个选项是pcallspmap。还有future。它们都记录在Clojure API

    【讨论】:

    • 啊,是的,代理机制是我忘记的东西。谢谢!
    • 别忘了 pmap 之类的!
    • "Clojure fns 是可运行的,因此通常按照您发布的方式使用它们,是的。"非常感谢您提供这些信息。
    • 小修正:send 将使用池中的线程。 send-off 将使用单独的线程。这就是他们之间的不同之处。如果您的操作阻塞,请使用send-off(否则您可能会阻塞代理池中的所有线程)。
    【解决方案2】:

    通常当我想在 Clojure 中启动一个线程时,我只使用future

    除了易于使用之外,它还有一个优点是您可以避免为了访问底层 Java 线程机制而必须执行任何混乱的 Java 互操作。

    使用示例:

    (future (some-long-running-function))
    

    这将在另一个线程中异步执行函数。

    (def a (future (* 10 10)))
    

    如果你想得到结果,只需取消对未来的引用,例如:

    @a
    => 100
    

    请注意,@a 将阻塞,直到未来的线程完成其工作。

    【讨论】:

    【解决方案3】:

    Clojure 编程直到第 167 页:“使用代理进行异步更新”才解决这个问题。

    在开始线程之前,请注意 Clojure 将自行执行多任务处理,有一半的机会。我已经编写了对并发性一无所知的程序,发现当条件合适时,它们占用的 CPU 不止一个。我知道这不是一个非常严格的定义:我还没有深入探讨过。

    但是对于那些你确实需要一个明确的单独活动的场合,Clojure 的答案之一显然是代理。

    (agent initial-state)

    将创建一个。就等待执行的代码块而言,它不像 Java 线程。相反,这是一项等待被分配工作的活动。您可以通过

    (send agent update-fn & args)

    例子有

    (def counter (agent 0))

    counter 是您的代理人姓名和代号;代理的状态是数字 0。

    设置完成后,您可以将工作发送给代理:

    (send counter inc)

    会告诉它把给定的函数应用到它的状态。

    您可以稍后通过取消引用将状态从代理中拉出:

    @counter 将为您提供从 0 开始的数字的当前值。

    函数await 可以让您对代理的活动执行类似join 的操作,如果它很长的话:

    (await & agents) 会等到它们都完成;还有另一个版本需要超时。

    【讨论】:

      【解决方案4】:

      是的,您在 Clojure 中启动 Java 线程的方式与您在 Clojure 中的方式类似。

      然而,真正的问题是:你为什么要这样做? Clojure 具有很多比线程更好的并发构造。

      如果您查看 Clojure 中的规范并发示例,Rich Hickey's ant colony simulation,您会发现它正好使用 0 个线程。整个源代码中对java.lang.Thread 的唯一引用是对Thread.sleep 的三个调用,其唯一目的是减慢模拟速度,以便您可以真正看到 UI 中发生了什么。

      所有的逻辑都在代理中完成:每只蚂蚁一个代理,动画一个代理,信息素蒸发一个代理。比赛场地是一个交易参考。不是线程也不是锁定在视线中。

      【讨论】:

      • 这是不正确的。 Clojure 当然使用引擎盖下的线程。但它提供了抽象来同步和协调这些线程,例如。期货、pmap 或代理。还有问题,你想在 java.util.concurrent 机器上使用线程。
      • 我会让 Jörg 怀疑他说线程时他的意思是线程 API,但这应该说清楚。
      • Clojure 在其内部运行时使用什么来实现代理、期货和诸如此类的东西是 Rich Hickey 的业务,而不是我的业务。今天 JVM 版本的 Clojure 恰好使用线程池这一事实与语言语义完全无关。 Tomorrow Rich 可能会改变主意并将它们实现为 Continuations,.NET 版本的 Clojure 可能将它们实现为 Tasks,ClojureScript(Clojure 的 JavaScript 版本)可能将它们实现为 HTML5 Web Workers,以及假设的未来 Erlang 托管版本可能将它们实现为演员。作为 Clojure 用户,我永远不会知道。
      • 不正确。如果任务足够小,pmap 的性能将低于map。抽象是有漏洞的,你应该始终了解底层发生了什么。
      • Clojure 经常故意泄漏。它公开了 Java,因此您可以利用它。 (Clojure 字符串 == Java 字符串等)Clojure 不会很快破坏与普通 Java 线程的兼容性,我猜。
      【解决方案5】:

      只是增加我的两分钱(7 年后):Clojure 函数实现了 IFn interface 扩展 Callable 以及 Runnable。因此,您可以简单地将它们传递给 Thread 这样的类。

      如果您的项目可能已经使用core.async,我更喜欢使用go 宏:

      (go func)
      

      这会在超轻量级IOC (inversion of control) thread 中执行func

      go [...] 会将身体变成状态机。在达到任何阻塞操作时,状态机将被“停放”并且实际的控制线程将被释放。 [...] 阻塞操作完成后,代码将恢复 [...]

      如果func 要做 I/O 或一些长时间运行的任务,你应该使用 thread,它也是 core.async 的一部分(查看 this 优秀的博文):

      (thread func)
      

      无论如何,如果您想坚持 Java 互操作语法,请考虑使用 ->(线程/箭头)宏:

      (-> (Thread. func) .start)
      

      【讨论】:

        【解决方案6】:

        使用future 通常是对线程的最简单的临时访问。完全取决于你想做什么:)

        【讨论】:

          【解决方案7】:

          (future f) 宏将表单 f 包装在 Callable 中(通过 fn*)并立即将其提交到线程池。

          如果您需要对 java.lang.Thread 对象的引用,例如,要将其用作 java.lang.Runtime 关闭挂钩,您可以像这样创建一个线程:

          (proxy [Thread] [] (run [] (println "running")))
          

          这不会启动线程,只会创建它。 要创建和运行线程,请将其提交到线程池或在其上调用 .start:

          (->
           (proxy [Thread] [] (run [] (println "running")))
           (.start))
          

          Brians 的回答也创建了一个线程,但不需要代​​理,因此非常优雅。另一方面,通过使用代理,我们可以避免创建 Callable。

          【讨论】:

            猜你喜欢
            • 2019-01-03
            • 1970-01-01
            • 2016-11-21
            • 1970-01-01
            • 2012-05-06
            • 1970-01-01
            • 1970-01-01
            • 2022-01-19
            • 2016-10-01
            相关资源
            最近更新 更多