zoukankan      html  css  js  c++  java
  • Clojure的并行与并发

    这次来聊聊clojure的并行与并发,如果你还不知clojure为何物,请翻翻我的上一篇推文。“并行”是指clojure对并行计算的支持(parallel computing),“并发”是其并发特性(concurrency)。用通俗的话来说,“并行”是同一时间做多件事情,“并发”是同一时间应对多件事情。举个例子,“并行”就类似于GPU做3D绘图,左手画圆、右手画方;“并发”就类似于web 服务器利用服务器的多个内核来同时处理来自用户的多个请求。如果还不够明白,请大家google一下wiki。^_^

    Clojure对并行计算支持的很好,而clojure的并发性其实一篇文章都写不完,因为它很有特色。clojure没有提供传统并发编程的元素,如:线程和锁。但clojure却提供了与线程和锁无关的、完全不同的4种并发编程模型,尽管你可以理解为这是clojure这么函数式语言基于java线程和锁的抽象。

    Clojure对并行计算的支持

    Clojure对并行计算的支持主要是通过并行类库与函数来提供支持,例如:reducer、pmap、pvalues、pcalls等,要注意的是:适用于CPU密集型任务,不是I/O密集或block的情形。reducer你没看错,的确是大数据的map-reduce的reducer,他们有联系?没有,但的确你可以把Clojure的并行计算包clojure.core.reducers用于大数据处理。想象一下,如果你需要处理一个大约 40G 的文件,对每一行文本进行解析并执行一些计算逻辑,最后写入数据库。你要怎么实现?是不是想到分而治之,但文件IO可能又是瓶颈,如何分割文件,然后放到内存,用clojure.core.reducers包来并行处理呢? 这是一个思路:先map后reduce。好了,回到正题。

    举个栗子:Java如何对一个数列求和,代码是不是这样的:

    public int sum(int[] numbers){

       int accumulator = 0;

       for(int n: numbers)

           accumulator += n;

       return accumulator;

    }

    Clojure是这样写:

    (defn reduce-sum [numbers]

       (reduce (fn [acc x] (+ acc x)) 0 numbers))

    这段代码用了clojure的reduce函数,其中3个参数:一个化简函数、一个初始值、一个集合。先用fn定义了一个匿名函数,接受两个参数并返回参数之和。然后后面的就是初始值和集合。其实,clojure有一个现成的函数+带代替fn这个匿名函数,所以代码可以继续简化:

    (defn sum [numbers]

       (reduce + numbers))

    上面都还没有涉及并行计算,现在,我们引入并行库clojure.core.reducers包,用里面的fold函数替换reduce:

    (ns sum.core

       (:require [clojure.core.reducers :as r]))

    (defn parallel-sum [numbers]

       (r/fold + numbers))

    测试一下1加到一亿,性能提升2.5倍。背后是什么魔法?大家可以看clojure的官方说明和源码,它底层是用了JVM 原生的 fork/join 工具而进行的优化,看源码它默认起的java线程数为n+2(为什么?照例cpu密集型的线程池配置应该是对cpu密集型的为等于cpu数,I/O密集型的为更多),其主要实现思路:

    1. 分而治之(Partitioning the reducible collection at a specified granularity (default = 512 elements))

    2. 应用到各个分区(Applying reduce to each partition)

    3. 分区计算结果集合并(Recursively combining each partition using Java's fork/join framework.)

    关于pmap等,此处不展开了。

    Clojure的并发特性

    前面提到:clojure没有提供传统并发编程的元素,如:线程和锁。但clojure却提供了与线程和锁无关的、完全不同的4种并发编程模型,尽管你可以理解为这是clojure这么函数式语言基于java线程和锁的抽象。这4种并发编程模型位:

    1. 线程本地vars(thread-local)

    2. 原子变量(atoms)

    3. 代理(agent)

    4. 引用(refs)和软件事务内存(ATM)

    Clojure中所有的数据都是非易变的,除非用相应的Var、Ref、Atom和Agent类型明确表示某数据是易变的。这提供了管理共享状态的安全机制,对于这一点要深刻理解。而对于上面这4种并发编程模型,笔者在仔细研究之后发现,最简洁适用的是第二种,所以笔者具体展开第二种原子变量,其它几种有兴趣的朋友自己去研究,我想理解了第二种其它的应该都容易理解。

    原子变量其实是在java.util.concurrent.atomic的基础上建立的。而java.util.concurrent.atomic背后其实用了CPU指令来实现的原子性保证,并使用了java.util.concurrent.atomicReference包提供的compareAndSet f方法,即CAS乐观比较重试法,所以内部没有锁。但java用cas也避免不了重试而clojure的原子变量为何能避免重试呢?原因就在于Clojure是函数式语言,其原子变量是无锁的,因为Clojure中所有的数据都是非易变的,是常量,它的值不是变化的,而是其数据结构被修改时总是保存了其之前的版本。

    举个栗子,用原子map:

    (def test (atom {}))

    (swap! test assoc :username "paul")

    (swap! test assoc :id 123)

    再举一个管理运动员的web服务,这个代码有点多,但很好理解:

    (def players (atom ()))

    (defn list-players []

       (response (json/encode @players)))

    (defn create-player [player-name]

       (swap! players conj player-name)

       (status (response "") 201))

    (defroutes app-routes

       (GET "/players" [] (list-players))

       (PUT "/players/:play-name" [player-name] (create-player player-name)))

    (defn -main [& args]

       (run-jetty (site app-routes) {:port 3000}))

    最后说一下如何学习.....

    经常有朋友问我如何自我提高,学习新知识?其实我们这一行要学习的东西真的很多,之前做无线的时候我还要学习客户端的东西(android/iOS/H5)和产品经理/UX的知识,现在要学docker看它的源码就要学Go语言。活到老,学到老嘛,stay hungry, stay foolish,永远把自己当成初学者,这样才能对新事物保持好奇,对所有观点持开放态度。

    我建议的学习方法:

    1. 闭环学习:从浏览器、网络协议、webserver、数据库一个闭环,你都有了解吗?长的闭环链条能赋予你全面分析和解决问题的能力,很容易定位和分析问题,也有了自己的知识体系。

    2. 顺藤摸瓜:比如Socket -> UNIX网络编程 -> TCP/IP协议,顺藤摸瓜,往往会发现自己研究的越多,不懂的越多。才发现知道了自己不知道....

    3. 量变到质变:学了很多,实践了吗?学以致用,没用,就真的没用了。量变形成质变,没量不可能质变,代码没写几行?写了一万行没总结提炼和思考也不可能质变。就像我们今天学了clojure的是否可以总结一下各种并发编程模型? 多实践、多思考。

    当然,前提是有兴趣,没有兴趣学习的话早点转行。

    (原文发布与微信公众号 rayisthinking, 原文链接:http://mp.weixin.qq.com/s?__biz=MzAxNTQ4NTIzNA==&mid=208631556&idx=1&sn=d404833e167dc46868a26f43d09187a1#rd)

  • 相关阅读:
    PHP计算两个绝对路径的相对路径
    mysql触发器的使用 想让b字段在更新的时候把旧数据保存到a字段中
    LHC大神问的矩阵转置问题
    母牛2年生小牛 5年后并死去的算法
    switch和continue的关系
    逐行读取文件示例
    安装Harbor管理镜像服务
    解决:ElasticSearch ClusterBlockException[blocked by: [FORBIDDEN/12/index read-only / allow delete (api)];
    Jenkins教程(五)构建Java服务Docker镜像
    Nacos高可用集群解决方案-Docker版本
  • 原文地址:https://www.cnblogs.com/Mainz/p/4605761.html
Copyright © 2011-2022 走看看