Clojure 的并发(一) Ref和STM
Clojure 的并发(二)Write Skew分析
Clojure 的并发(三)Atom、缓存和性能
Clojure 的并发(四)Agent深入分析和Actor
Clojure 的并发(五)binding和let
Clojure的并发(六)Agent可以改进的地方
Clojure的并发(七)pmap、pvalues和pcalls
Clojure的并发(八)future、promise和线程
前面几节基本介绍Clojure对并发的支持,估计这个系列还能写个两三节,介绍下一些常见的concurrent function的使用。谈了很多优点,现在想谈谈clojure的一些缺憾,例如Agent系统。
Agent在前面已经介绍过了,用于异步更新,基本的原理是将更新操作封装为action,提交给线程池处理。内部有两个线程池,固定大小(cpus+2)的线程池用于处理send发送的action,而send-off发送的action则是由一个Cached ThreadPool处理的。因此,如果你的更新操作很耗时或者会阻塞(如IO操作),那么通常是建议使用send-off,而send适合一些计算密集型的更新操作。两个线程池的声明如下(Agent.java):
说说我认为Agent可以改进的地方。
首先是从实践角度出发,通常我们要求new一个线程池的时候,要设置线程池内线程的name,以方便查看和调试。因此Clojure这里简单的new线程池是一个可以改进的地方,应当自定义一个ThreadFactory,给clojure的Agent线程提供明确的名称。
其次,由于这两个线程池是全局的,因此clojure提供了shutdown-agents的方法用于关闭线程池。但是由于这些线程池内的线程并非daemon,因此如果你没有明确地调用shutdown-agents,jvm也可以正常退出。
我们都知道,如果还有dadmon线程没有终止,JVM是无法退出的
。如果JVM只剩下daemon线程,那么jvm就会自动退出。从实践角度,应当明确地要求用户调用shutdown-agents来关闭Agent系统,妥善终止线程,并且Agent的线程池应当延迟初始化,只在必要的时候创建,而非现在的静态变量。所以,在实现ThreadFactory的时候,应当设置生成的线程为daemon。
第三,同样由于线程池是全局的,关闭了却没有办法重新启动,这不能不说是一个缺憾。Clojure没有提供重新启动的方法。
第四,线程池简单地分为两类,从理论上足以满足大部分应用的要求。但是在real world的应用上,我们通常不敢用CachedThreadPool,这是为了防止内存不受控,导致线程创建过多直接OOM。通常我们会使用固定大小的线程池,但是clojure固定大小的线程池只有一个,并且大小写死为cpus+2,这就没有了控制的余地。我还是希望clojure能提供允许自定义Agent线程池的方法,可以在创建的时候传入线程池,如:
或者提供新的API,如set-executor!来设置agent使用的线程池,如果没有自定义线程池再使用全局的。当然也需要提供一个关闭agent自定义线程池的API:
需要自定义线程池是另一个原因是为了最大化地发挥线程池的效率,我们知道,线程池只有在执行“同构”任务的时候才能发挥最大的效率,如果有的 action快,有的action慢,那么该快的快不起来,慢的却挤占了快的action的执行时间。通过给Agent设定自己的线程池某些程度上可以解决这个问题。
Agent的整个模型是很优雅的,但是确实还有这些地方不是特别让人满意,希望以后会得到改进。
Clojure 的并发(二)Write Skew分析
Clojure 的并发(三)Atom、缓存和性能
Clojure 的并发(四)Agent深入分析和Actor
Clojure 的并发(五)binding和let
Clojure的并发(六)Agent可以改进的地方
Clojure的并发(七)pmap、pvalues和pcalls
Clojure的并发(八)future、promise和线程
前面几节基本介绍Clojure对并发的支持,估计这个系列还能写个两三节,介绍下一些常见的concurrent function的使用。谈了很多优点,现在想谈谈clojure的一些缺憾,例如Agent系统。
Agent在前面已经介绍过了,用于异步更新,基本的原理是将更新操作封装为action,提交给线程池处理。内部有两个线程池,固定大小(cpus+2)的线程池用于处理send发送的action,而send-off发送的action则是由一个Cached ThreadPool处理的。因此,如果你的更新操作很耗时或者会阻塞(如IO操作),那么通常是建议使用send-off,而send适合一些计算密集型的更新操作。两个线程池的声明如下(Agent.java):
1
final
public
static
ExecutorService pooledExecutor
=
2 Executors.newFixedThreadPool( 2 + Runtime.getRuntime().availableProcessors());
3
4 final public static ExecutorService soloExecutor = Executors.newCachedThreadPool();
2 Executors.newFixedThreadPool( 2 + Runtime.getRuntime().availableProcessors());
3
4 final public static ExecutorService soloExecutor = Executors.newCachedThreadPool();
说说我认为Agent可以改进的地方。
首先是从实践角度出发,通常我们要求new一个线程池的时候,要设置线程池内线程的name,以方便查看和调试。因此Clojure这里简单的new线程池是一个可以改进的地方,应当自定义一个ThreadFactory,给clojure的Agent线程提供明确的名称。
其次,由于这两个线程池是全局的,因此clojure提供了shutdown-agents的方法用于关闭线程池。但是由于这些线程池内的线程并非daemon,因此如果你没有明确地调用shutdown-agents,jvm也可以正常退出。
第三,同样由于线程池是全局的,关闭了却没有办法重新启动,这不能不说是一个缺憾。Clojure没有提供重新启动的方法。
第四,线程池简单地分为两类,从理论上足以满足大部分应用的要求。但是在real world的应用上,我们通常不敢用CachedThreadPool,这是为了防止内存不受控,导致线程创建过多直接OOM。通常我们会使用固定大小的线程池,但是clojure固定大小的线程池只有一个,并且大小写死为cpus+2,这就没有了控制的余地。我还是希望clojure能提供允许自定义Agent线程池的方法,可以在创建的时候传入线程池,如:
(agent :executor (java.util.concurrent.Executors
/
newFixedThreadPool
2
))
或者提供新的API,如set-executor!来设置agent使用的线程池,如果没有自定义线程池再使用全局的。当然也需要提供一个关闭agent自定义线程池的API:
(shutdown
-
agent agent)
需要自定义线程池是另一个原因是为了最大化地发挥线程池的效率,我们知道,线程池只有在执行“同构”任务的时候才能发挥最大的效率,如果有的 action快,有的action慢,那么该快的快不起来,慢的却挤占了快的action的执行时间。通过给Agent设定自己的线程池某些程度上可以解决这个问题。
Agent的整个模型是很优雅的,但是确实还有这些地方不是特别让人满意,希望以后会得到改进。