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的思路类似批量处理来提高系统的吞吐量。


你可能感兴趣的:(Clojure的并发(七)pmap、pvalues和pcalls)