Clojure的并发(七)pmap、pvalues和pcalls

简介:
Clojure 的并发(一) Ref和STM
Clojure 的并发(二)Write Skew分析
Clojure 的并发(三)Atom、缓存和性能
Clojure 的并发(四)Agent深入分析和Actor
Clojure 的并发(五)binding和let
Clojure的并发(六)Agent可以改进的地方
Clojure的并发(七)pmap、pvalues和pcalls
Clojure的并发(八)future、promise和线程

七、并发函数pmap、pvalues和pcalls

 1、pmap是map的进化版本,map将function依次作用于集合的每个元素,pmap也是这样,但是它对于每个集合中的元素都是提交给一个线程去执行function,也就是并行地对集合里的元素执行指定的函数。通过一个例子来解释下。我们先定义一个make-heavy函数用于延时执行某个函数:
(defn make - heavy [f]
        (fn [
&  args]
            (Thread
/ sleep  1000 )
            (apply f args)))

make-heavy接受一个函数f作为参数,返回一个新的函数,它延时一秒才实际执行f。我们利用make-heavy包装inc,然后执行下map:

user =>  (time (doall (map (make - heavy inc) [ 1   2   3   4   5 ])))
" Elapsed time: 5005.115601 msecs "
(
2   3   4   5   6 )

可以看到总共执行了5秒,这是因为map依次将包装后的inc作用在每个元素上,每次调用都延时一秒,总共5个元素,因此延时了5秒左右。这里使用doall,是为了强制map返回的lazy-seq马上执行。

如果我们使用pmap替代map的话:
user =>  (time (doall (pmap (make - heavy inc) [ 1   2   3   4   5 ])))
" Elapsed time: 1001.146444 msecs "
(
2   3   4   5   6 )

果然快了很多,只用了1秒多,显然pmap并行地将make-heavy包装后的inc作用在集合的5个元素上,总耗时就接近于于单个调用的耗时,也就是一秒。


2、pvalues和pcalls是在pmap之上的封装,pvalues是并行地执行多个表达式并返回执行结果组成的LazySeq,pcalls则是并行地调用多个无参数的函数并返回调用结果组成的LazySeq。

user =>  (pvalues ( +   1   2 ) ( -   1   2 ) ( *   1   2 ) ( /   1   2 ))
(
3   - 1   2   1 / 2 )

user =>  (pcalls #(println  " hello " ) #(println  " world " ))
hello
world
(nil nil)

3、pmap的并行,从实现上来说,是集合有多少个元素就使用多少个线程:
 1  (defn pmap
 2    {:added  " 1.0 " }
 3    ([f coll]
 4     (let [n ( +   2  (.. Runtime getRuntime availableProcessors))
 5           rets (map #(future (f  % )) coll)
 6           step (fn step [[x  &  xs :as vs] fs]
 7                  (lazy - seq
 8                   ( if - let [s (seq fs)]
 9                     (cons (deref x) (step xs (rest s)))
10                     (map deref vs))))]
11       (step rets (drop n rets))))
12    ([f coll  &  colls]
13     (let [step (fn step [cs]
14                  (lazy - seq
15                   (let [ss (map seq cs)]
16                     (when (every ?  identity ss)
17                       (cons (map first ss) (step (map rest ss)))))))]
18       (pmap #(apply f  % ) (step (cons coll colls))))))

在第5行,利用map和future将函数f作用在集合的每个元素上,future是将函数f(实现callable接口)提交给Agent的CachedThreadPool处理, 跟agent的send-off共用线程池

但是由于有chunked-sequence的存在, 实际上调用的线程数不会超过chunked的大小,也就是32。事实上,pmap启动多少个线程取决于集合的类型,对于chunked-sequence,是以32个元素为单位来批量执行,通过下面的测试可以看出来,range返回的是一个chunked-sequence,clojure 1.1引入了chunked-sequence,目前那些返回LazySeq的函数如map、filter、keep等都是返回chunked-sequence:

user =>  (time (doall (pmap (make - heavy inc) (range  0   32 ))))
" Elapsed time: 1003.372366 msecs "
(
1   2   3   4   5   6   7   8   9   10   11   12   13   14   15   16   17   18   19   20   21   22   23   24   25   26   27   28   29   30   31   32 )

user
=>  (time (doall (pmap (make - heavy inc) (range  0   64 ))))
" Elapsed time: 2008.153617 msecs "
(
1   2   3   4   5   6   7   8   9   10   11   12   13   14   15   16   17   18   19   20   21   22   23   24   25   26   27   28   29   30   31   32   33   34   35   36   37   38   39   40   41   42   43   44   45   46   47   48   49   50   51   52   53   54   55   56   57   58   59   60   61   62   63   64 )


可以看到,对于32个元素,执行(make-heavy inc)耗费了一秒左右;对于64个元素,总耗时是2秒,这可以证明64个元素是分为两个批次并行执行,一批32个元素,启动32个线程(可以通过jstack查看)。


并且pmap的执行是半延时的(semi-lazy),前面的总数-(cpus+2)个元素是一个一个deref(future通过deref来阻塞获取结果),后cpus+2个元素则是一次性调用map执行deref。

4、pmap的适用场景取决于将集合分解并提交给线程池并行执行的代价是否低于函数f执行的代价,如果函数f的执行代价很低,那么将集合分解并提交线程的代价可能超过了带来的好处,pmap就不一定能带来性能的提升。pmap只适合那些计算密集型的函数f,计算的耗时超过了协调的代价。

5、关于chunked-sequence可以看看这篇报道,也可以参考Rich Hickey的PPT。chunk sequence的思路类似批量处理来提高系统的吞吐量。

文章转自庄周梦蝶  ,原文发布时间2010-08-04

目录
相关文章
|
10月前
|
存储 安全 算法
网络安全与信息安全:漏洞、加密技术及安全意识的重要性
如今的网络环境中,网络安全威胁日益严峻,面对此类问题,除了提升相关硬件的安全性、树立法律法规及行业准则,增强网民的网络安全意识的重要性也逐渐凸显。本文梳理了2000年以来有关网络安全意识的研究,综述范围为中国知网中篇名为“网络安全意识”的期刊、硕博论文、会议论文、报纸。网络安全意识的内涵是在“网络安全”“网络安全风险”等相关概念的发展中逐渐明确并丰富起来的,但到目前为止并未出现清晰的概念界定。此领域内的实证研究主要针对网络安全意识现状与问题,其研究对象主要是青少年。网络安全意识教育方面,很多学者总结了国外的成熟经验,但在具体运用上仍缺乏考虑我国的实际状况。 内容目录: 1 网络安全意识的相关
|
安全 测试技术 持续交付
微服务的测试策略
【8月更文第29天】随着微服务架构的普及,测试变得尤为重要,因为它有助于确保各个独立的服务都能正确运行并且能够协同工作。本文将介绍一种全面的测试策略,包括单元测试、集成测试和端到端测试,以及如何为微服务应用编写这些测试。
453 1
|
负载均衡 Java Apache
【微服务系列笔记】Feign
Feign是一个声明式的伪HTTP客户端,它使得HTTP请求变得更简单。使用Feign,只需要创建一个接口并注解。Feign默认集成了Ribbon,并和Eureka结合,默认实现了负载均衡的效果。 OpenFeign 是SpringCloud在Feign的基础上支持了SpringMVC的注解。
326 8
|
11月前
|
域名解析 弹性计算 缓存
阿里云国际云服务器全局流量分析功能详细介绍
阿里云国际云服务器全局流量分析功能详细介绍
|
机器学习/深度学习 人工智能 自然语言处理
NLP技术
【7月更文挑战第8天】NLP技术
453 2
|
安全 Android开发
反编译之将脱壳后的dex文件重新打包成apk
反编译之将脱壳后的dex文件重新打包成apk
2383 0
|
存储 分布式计算 OLAP
AnalyticDB基于Apache Hudi构建低成本Lakehouse实践
基于低成本对象存储OSS + Apache Hudi 构建Lakehouse的挑战与实践
|
JSON 前端开发 Java
MARKDOWN上传图片的基本实现
MARKDOWN上传图片的基本实现
MARKDOWN上传图片的基本实现