我读了很多有关Clojure在并发方面非常出色的文章,但是我读过的所有教程都没有解释如何创建线程。您只需执行(.start (Thread. func)),还是我错过了其他方法?
我读了很多有关Clojure在并发方面非常出色的文章,但是我读过的所有教程都没有解释如何创建线程。您只需执行(.start (Thread. func)),还是我错过了其他方法?
Clojure的fn
函数是可运行的,因此通常可以像您发布的方式一样使用它们。
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中都有文档记录。
通常在Clojure中启动线程时,我只是使用future。
除了使用简单外,这种方法的优点是避免了访问底层Java线程机制时需要进行任何混乱的Java互操作。
示例用法:
(future (some-long-running-function))
这将在另一个线程中异步执行该函数。
(def a (future (* 10 10)))
如果你想获取结果,只需取消引用future对象,例如:
@a
=> 100
请注意,@a会一直阻塞,直到未来线程完成其工作。Programming 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中的典型并发示例Rich Hickey's ant colony simulation,您会发现它完全不使用线程。在整个源代码中,对java.lang.Thread
的唯一引用是三次调用Thread.sleep
,其唯一目的是减缓模拟速度,以便您实际上可以看到UI中正在发生的事情。
所有逻辑都在代理中完成:每只蚂蚁一个代理,动画一个代理,信息素蒸发一个代理。游戏场地是一个事务性ref。没有线程或锁可见。
pmap
的性能会低于 map
。抽象是有漏洞的,你应该始终了解底层发生了什么。 - Mike Douglaspmap
需要理解生成和协调线程的成本,所以你已经逃离了“理想并行应用”的世界。这就是我所说的“leaky”的含义;这并不是对语言的批评。正如Brian提到的,这通常是一种可取的方面。 - Mike Douglas仅供参考(7年后):Clojure函数实现了IFn
接口,该接口扩展了Callable
和Runnable
。因此,您可以将它们简单地传递给Thread
等类。
如果您的项目可能已经使用core.async,我建议使用go
宏:
(go func)
func
:
如果go [...]将把主体变成状态机。 在达到任何阻塞操作时,状态机将被“停放”,实际的控制线程将被释放。 [...]当阻塞操作完成时,代码将会恢复[...]
func
将要执行I/O或一些长时间运行的任务,则应使用thread
,它也是core.async的一部分(请查看this出色的博客文章):(thread func)
->
(线程/箭头)宏:(-> (Thread. func) .start)
使用future通常是最简单的临时线程访问方式。完全取决于您想要做什么 :)
(future f)
宏将表格f用Callable(通过fn *)包装并立即提交到线程池中。如果需要引用java.lang.Thread对象(例如,将其用作java.lang.Runtime关闭挂钩),可以按以下方式创建Thread:(proxy [Thread] [] (run [] (println "running")))
这并不会立即启动线程,只是创建了线程。 要创建并运行线程,请将其提交到线程池或调用 .start 方法:
(->
(proxy [Thread] [] (run [] (println "running")))
(.start))
send
将使用线程池中的一个线程。send-off
将使用一个独立的线程。这就是它们之间的区别。如果您的操作会阻塞,请使用send-off
(否则您可能会阻塞代理池中的所有线程)。 - Blake Miller