Clojure的并发(六)Agent可以改进的地方
2023-03-14 10:32:08 时间
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:
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也可以正常退出。我们都知道,如果还有dadmon线程没有终止,JVM是无法退出的。如果JVM只剩下daemon线程,那么jvm就会自动退出。从实践角度,应当明确地要求用户调用shutdown-agents来关闭Agent系统,妥善终止线程,并且Agent的线程池应当延迟初始化,只在必要的时候创建,而非现在的静态变量。所以,在实现ThreadFactory的时候,应当设置生成的线程为daemon。
第三,同样由于线程池是全局的,关闭了却没有办法重新启动,这不能不说是一个缺憾。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的整个模型是很优雅的,但是确实还有这些地方不是特别让人满意,希望以后会得到改进。
文章转自庄周梦蝶 ,原文发布时间 2010-07-30
相关文章
- Java要抛弃祖宗的基业,Java程序员危险了!
- 十大 Java 语言特性
- JVM 三色标记算法,原来是这么回事!
- 聊聊 Spring 事务控制策略以及 @Transactional 失效问题避坑
- 写给 Java 程序员的前端 Promise 教程
- 写给 Java 程序员的前端 Promise 教程,你学会了吗?
- Java 中为什么不全部使用 Static 方法?
- Java 池化技术你了解多少?
- Java 服务 Docker 容器化优秀实践
- Spring Boot + EasyExcel导入导出,简直太好用了!
- 我们一起聊聊 Java 内存泄漏
- CentOS 下安装 Docker 极简教程
- JDK 19 功能集冻结:Java 19 只有七个新特性
- 关于 CMS 垃圾回收器,你真的懂了吗?
- 为什么会有这么多编程语言?
- 改善Java代码的八个建议
- 接口流量突增,如何做好性能优化?
- Java 以编程方式创建JAR文件
- POJO、Java Bean是如何定义的
- Spring 的 Bean 明明设置了 Scope 为 Prototype,为什么还是只能获取到单例对象?