【发布时间】:2010-12-18 14:55:00
【问题描述】:
我读过很多关于 Clojure 在并发方面有多棒的文章,但我读过的教程都没有真正解释如何创建线程。你只是做(.start(Thread.func)),还是我错过了另一种方式?
【问题讨论】:
标签: multithreading concurrency clojure
我读过很多关于 Clojure 在并发方面有多棒的文章,但我读过的教程都没有真正解释如何创建线程。你只是做(.start(Thread.func)),还是我错过了另一种方式?
【问题讨论】:
标签: multithreading concurrency clojure
Clojure fns 是 Runnable,因此通常按照您发布的方式使用它们,是的。
user=> (dotimes [i 10] (.start (Thread. (fn [] (println i)))))
0
1
2
4
5
3
6
7
8
9
nil
另一种选择是使用agents,在这种情况下,您将使用send 或send-off,它会使用池中的线程。
user=> (def a (agent 0))
#'user/a
user=> (dotimes [_ 10] (send a inc))
nil
;; ...later...
user=> @a
10
另一个选项是pcalls 和pmap。还有future。它们都记录在Clojure API。
【讨论】:
send 将使用池中的线程。 send-off 将使用单独的线程。这就是他们之间的不同之处。如果您的操作阻塞,请使用send-off(否则您可能会阻塞代理池中的所有线程)。
通常当我想在 Clojure 中启动一个线程时,我只使用future。
除了易于使用之外,它还有一个优点是您可以避免为了访问底层 Java 线程机制而必须执行任何混乱的 Java 互操作。
使用示例:
(future (some-long-running-function))
这将在另一个线程中异步执行函数。
(def a (future (* 10 10)))
如果你想得到结果,只需取消对未来的引用,例如:
@a
=> 100
请注意,@a 将阻塞,直到未来的线程完成其工作。
【讨论】:
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) 会等到它们都完成;还有另一个版本需要超时。
【讨论】:
是的,您在 Clojure 中启动 Java 线程的方式与您在 Clojure 中的方式类似。
然而,真正的问题是:你为什么要这样做? Clojure 具有很多比线程更好的并发构造。
如果您查看 Clojure 中的规范并发示例,Rich Hickey's ant colony simulation,您会发现它正好使用 0 个线程。整个源代码中对java.lang.Thread 的唯一引用是对Thread.sleep 的三个调用,其唯一目的是减慢模拟速度,以便您可以真正看到 UI 中发生了什么。
所有的逻辑都在代理中完成:每只蚂蚁一个代理,动画一个代理,信息素蒸发一个代理。比赛场地是一个交易参考。不是线程也不是锁定在视线中。
【讨论】:
Tasks,ClojureScript(Clojure 的 JavaScript 版本)可能将它们实现为 HTML5 Web Workers,以及假设的未来 Erlang 托管版本可能将它们实现为演员。作为 Clojure 用户,我永远不会知道。
pmap 的性能将低于map。抽象是有漏洞的,你应该始终了解底层发生了什么。
只是增加我的两分钱(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)
【讨论】:
使用future 通常是对线程的最简单的临时访问。完全取决于你想做什么:)
【讨论】:
(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。
【讨论】: