上节我们讨论了并行运算组件库的基础设计,实现了并行运算最基本的功能:创建新的线程并提交一个任务异步执行。并行运算类型的基本表达形式如下:

 import java.util.concurrent._
object Par {
type Par[A] = ExecutorService => Future[A]
def run[A](es: ExecutorService)(pa: Par[A]): Future[A] = pa(es)
//> run: [A](es: java.util.concurrent.ExecutorService)(pa: ch71.Par.Par[A])java.
//| util.concurrent.Future[A]
def unit[A](a: A): Par[A] = {
es => new Future[A] {
def get = a
def get(t: Long, u: TimeUnit) = get
def isDone = true
def isCancelled = false
def cancel(evenIsRunning: Boolean) = false
}
} //> unit: [A](a: A)ch71.Par.Par[A]
def fork[A](pa: Par[A]): Par[A] = { //注意这里有个错误?
es => es.submit(new Callable[A] {
def call: A = run(es)(pa).get
})
}
def async[A](a: => A): Par[A] = fork(unit(a)) }

实际上我们已经实现了两项最基本的函数:

1、unit[A](a: A): Par[A] : 我们硬生生的按照Par的类型款式造了一个Future实例,这样我们才可以用Future.get的形式读取运算结果值。看看这个例子:unit(42+1),在调用函数unit时由于传入参数是即时计算的,所以在进入unit前已经完成了计算结果43。然后人为的把这个结果赋予Future.get,这样我们就可以和真正的由ExecutorService返回的Future一样用同样的方式读取结果。所以说unit纯粹是一个改变格式的升格函数,没有任何其它作用。

2、async[A](a: => A): Par[A]:这个async函数把表达式a提交到主线程之外的另一个线程。新的线程由ExecutorService提供,我们无须理会,这样可以实现线程管理和并行运算组件库的松散耦合。由于async的传人函数是延后计算类型,所以我们可以把表达式a提交给另一个线程去运算。

那么我们用例子来示范一下:

   val es = Executors.newCachedThreadPool()  //线程由jvm提供,我们无须理会
//> es : java.util.concurrent.ExecutorService = java.util.concurrent.ThreadPool
//| Executor@19dfb72a[Running, pool size = 0, active threads = 0, queued tasks =
//| 0, completed tasks = 0]
val a = unit({println(Thread.currentThread.getName); +})
//> main
//| a : ch71.Par.Par[Int] = <function1>
val b = async({println(Thread.currentThread.getName); +})
//> main
//| b : ch71.Par.Par[Int] = <function1>
run(es)(a).get //> res0: Int = 43
run(es)(b).get //> res1: Int = 43
es.shutdown()

看到问题了吗?用run运算a,b时没有显示println,而这个println在申明val a, val b 时已经执行了。对unit这可以理解:参数是即时计算的,所以println和结果43都在进入函数之前运算了(然后放到Future.get)。但是async的参数不是延迟计算的吗?我们再看清楚:async(a: => A) >>> fork(unit(a)),到fork函数参数unit(a)就立即计算了。所以 fork(pa: => Par[A])才可以保证在提交任务前都不会计算表达式a。我们必须把fork的函数款式改一下:

 def fork[A](pa: => Par[A]): Par[A] = {
es => es.submit(new Callable[A] {
def call: A = run(es)(pa).get
})
} //> fork: [A](pa: ch71.Par.Par[A])ch71.Par.Par[A]

再运行一下例子:

  val es = Executors.newCachedThreadPool()  //线程由jvm提供,我们无须理会
//> es : java.util.concurrent.ExecutorService = java.util.concurrent.ThreadPool
//| Executor@19dfb72a[Running, pool size = 0, active threads = 0, queued tasks =
//| 0, completed tasks = 0]
val a = unit({println(Thread.currentThread.getName); +})
//> main
//| a : ch71.Par.Par[Int] = <function1>
val b = async({println(Thread.currentThread.getName); +})
//> b : ch71.Par.Par[Int] = <function1>
run(es)(a).get //> res0: Int = 43
run(es)(b).get //> pool-1-thread-1
//| res1: Int = 43
es.shutdown()

看看结果:unit在主线程main运行,而async则在pool-1-thread-1这个非主线程内运行。

实现异步运算才是并行运算的第一步。并行运算顾名思义就是把一个大任务分解成几个较小任务然后同时异步运算后再把结果结合起来。我们用伪代码描述一下并行运算思路:

  //伪代码
val big10sencondJob = ??? //一个10秒运算
val small5sJob1 = split big10sencondJob in half //分解成两个 5秒运算
val small5sJob2 = split big10sencondJob in half //分解成两个 5秒运算
val fa = run small5sJob1 //立即返回future 但开始运算 5 秒
val fb = run small5sJob2 //立即返回future 但开始运算 5 秒
val sum = fa.get + fb.get //等待5秒后可以得出结果

看来用以上方式是可以得到并行运算的效果(10秒到5秒区别)。但我们采用了串指令(imperative)方式实现。当然我们必须考虑用泛函方式来实现并行运算的启动及结果抽取。

先用泛函方式启动并行运算。如果我们并行启动两个运算:

  def map2[A,B,C](pa: Par[A], pb: Par[B])(f: (A,B) => C): Par[C]

map2并行启动pa,pb然后把它们的结果用函数f结合。看起来很美。那么我们先试着把它实现了:

  def map2[A,B,C](pa: Par[A], pb: Par[B])(f: (A,B) => C): Par[C] = {
import TimeUnit.NANOSECONDS
es => new Future[C] {
val fa = run(es)(pa) //在这里按pa的定义来确定在那个线程运行。如果pa是fork Par则在非主线程中运行
val fb = run(es)(pb)
def get = f(fa.get, fb.get)
def get(timeOut: Long, timeUnit: TimeUnit) = {
val start = System.nanoTime
val a = fa.get
val end = System.nanoTime
//fa.get用去了一些时间。剩下给fb.get的timeout值要减去
val b = fb.get(timeOut - timeUnit.convert((end - start), NANOSECONDS) , timeUnit)
f(a,b)
}
def isDone = fa.isDone && fb.isDone
def isCancelled = fa.isCancelled && fb.isCancelled
def cancel(evenIsRunning: Boolean) = fa.cancel(evenIsRunning) || fb.cancel(evenIsRunning)
}
} //> map2: [A, B, C](pa: ch71.Par.Par[A], pb: ch71.Par.Par[B])(f: (A, B) => C)ch
//| 71.Par.Par[C]

在map2的实现里我们人为地建了个Future[C]。但在建的过程中我们运行了pa,pb的计算。如果我们对pa或pb有运算超时要求的话,就必须计算每次运算所使用的时间。所以Future[C]是符合pa,pb的运算要求的。

我们先试着同时运算41+2,33+4两个计算:

 val es = Executors.newCachedThreadPool()  //线程由jvm提供,我们无须理会
//> es : java.util.concurrent.ExecutorService = java.util.concurrent.ThreadPoo
//| lExecutor@19dfb72a[Running, pool size = 0, active threads = 0, queued tasks
//| = 0, completed tasks = 0]
map2(async({println(Thread.currentThread.getName); +}),
async({println(Thread.currentThread.getName); +}))
{(a,b) => {println(Thread.currentThread.getName); a+b}}(es).get
//> pool-1-thread-1
//| pool-1-thread-2
//| main
//| res0: Int = 80

啊!pa,pb分别在不同的非主线程中运行了。但函数f的运行是在主线程main中运行的。我们试着把这个也放到非主线程中:

 fork { map2(async({println(Thread.currentThread.getName); +}),
async({println(Thread.currentThread.getName); +}))
{(a,b) => {println(Thread.currentThread.getName); a+b}}}(es).get
//> pool-1-thread-2
//| pool-1-thread-3
//| pool-1-thread-1
//| res0: Int = 80

现在所有的计算都是在不同的非主线程中运算的了,清楚了吧。

两个以上并行运算可以通过map2来实现:

   def map3[A,B,C,D](pa: Par[A], pb: Par[B], pc: Par[C])(f: (A,B,C) => D): Par[D] = {
map2(pa,map2(pb,pc){(b,c) => (b,c)}){(a,bc) => {
val (b,c) = bc
f(a,b,c)
}}
}
def map4[A,B,C,D,E](pa: Par[A], pb: Par[B], pc: Par[C], pd: Par[D])(f: (A,B,C,D) => E): Par[E] = { //| 71.Par.Par[C]
map2(pa,map2(pb,map2(pc,pd){(c,d) => (c,d)}){(b,cd) => (b,cd)}){(a,bcd) => {
val (b,(c,d)) = bcd
f(a,b,c,d)
}}
}
def map5[A,B,C,D,E,F](pa: Par[A], pb: Par[B], pc: Par[C], pd: Par[D], pe: Par[E])(f: (A,B,C,D,E) => F): Par[F] = { //| 71.Par.Par[C]
map2(pa,map2(pb,map2(pc,map2(pd,pe){(d,e) => (d,e)}){(c,de) => (c,de)}){(b,cde) => (b,cde)}){(a,bcde) => {
val (b,(c,(d,e))) = bcde
f(a,b,c,d,e)
}}
}

再看个例子:如果一个并行运算的表达式是个List[Int],即 Par[List[Int]]。 如何对内部的List[Int]进行排序?

 //我们可以run pa, get list 后进行排序,然后再封装进Future[List[Int]]
def sortPar(pa: Par[List[Int]]): Par[List[Int]] = {
es => {
val l = run(es)(pa).get
new Future[List[Int]] {
def get = l.sorted
def isDone = true
def isCancelled = false
def get(t: Long, u: TimeUnit) = get
def cancel(e: Boolean) = false
}
}
}
//也可以用map2来实现。因为map2可以启动并行运算,也可以对par内元素进行操作。但操作只针对一个par,
//我们用unit(())替代第二个par。现在我们可以对一个par的元素进行操作了
def sortedPar(pa: Par[List[Int]]): Par[List[Int]] = {
map2(pa,unit(())){(a,_) => a.sorted}
}
//map是对一个par的元素进行变形操作,我们同样可以用map2实现了
def map[A,B](pa: Par[A])(f: A => B): Par[B] = {
map2(pa,unit(())){(a,_) => f(a) }
}
//然后用map去对Par[List[Int]]排序
def sortParByMap(pa: Par[List[Int]]): Par[List[Int]] = {
map(pa){_.sorted}
}

看看运行结果:

 sortPar(async({println(Thread.currentThread.getName); List(,,,)}))(es).get
//> pool-1-thread-1
//| res3: List[Int] = List(1, 2, 3, 4)
sortParByMap(async({println(Thread.currentThread.getName); List(,,,)}))(es).get
//> pool-1-thread-1
//| res4: List[Int] = List(1, 2, 3, 4)

实际上map2做了两件事:启动了两个并行运算、对运算结果进行了处理。这样说map2是可以被分解成更基本的组件函数:

 //启动两项并行运算
def product[A,B](pa: Par[A], pb: Par[B]): Par[(A,B)] = {
es => unit((run(es)(pa).get, run(es)(pb).get))(es)
} //> product: [A, B](pa: ch71.Par.Par[A], pb: ch71.Par.Par[B])ch71.Par.Par[(A, B
//| )]
//处理运算结果
def map[A,B](pa: Par[A])(f: A => B): Par[B] = {
es => unit(f(run(es)(pa).get))(es)
} //> map: [A, B](pa: ch71.Par.Par[A])(f: A => B)ch71.Par.Par[B]
//再组合map2
def map2_pm[A,B,C](pa: Par[A], pb: Par[B])(f: (A,B) => C): Par[C] = {
map(product(pa, pb)){a => f(a._1, a._2)}
} //> map2_pm: [A, B, C](pa: ch71.Par.Par[A], pb: ch71.Par.Par[B])(f: (A, B) => C
//| )ch71.Par.Par[C]

我们还可以把函数A => B转换成A => Par[B],意思是把对A的运算变成并行运算Par[B]:

   def asyncF[A,B](f: A => B): A => Par[B] = a => async(f(a))
//> asyncF: [A, B](f: A => B)A => ch71.Par.Par[B]

用asyncF应该可以把对一个List的处理函数变成并行运算:

 def parMap[A,B](as: List[A])(f: A => B): Par[List[B]]

用 map(as){asyncF(f)}可以得到List[Par[B]]。再想办法List[Par[B]] >>> Par[List[B]],这不就是我们经常遇到的那个sequence函数的类型款式吗。那我们就先实现了par的sequence函数吧:

  //用递归法实现
def sequence_r[A](lp: List[Par[A]]): Par[List[A]] = {
lp match {
case Nil => unit(List())
case h::t => map2(h,fork(sequence_r(t))){_ :: _}
}
} //> sequence_r: [A](lp: List[ch71.Par.Par[A]])ch71.Par.Par[List[A]]
//用foldLeft
def sequenceByFoldLeft[A](lp: List[Par[A]]): Par[List[A]] = {
lp.foldLeft(unit[List[A]](Nil)){(t,h) => map2(h,t){_ :: _}}
} //> sequenceByFoldLeft: [A](lp: List[ch71.Par.Par[A]])ch71.Par.Par[List[A]]
//用foldRight
def sequenceByFoldRight[A](lp: List[Par[A]]): Par[List[A]] = {
lp.foldRight(unit[List[A]](Nil)){(h,t) => map2(h,t){_ :: _}}
} //> sequenceByFoldRight: [A](lp: List[ch71.Par.Par[A]])ch71.Par.Par[List[A]]
//用IndexedSeq切成两半来实现
def sequenceBalanced[A](as: IndexedSeq[Par[A]]): Par[IndexedSeq[A]] = {
if (as.isEmpty) unit(Vector())
else if (as.length == ) map(as.head){a => Vector(a)}
else {
val (l,r) = as.splitAt(as.length / )
map2(sequenceBalanced(l),sequenceBalanced(r)){_ ++ _}
}
} //> sequenceBalanced: [A](as: IndexedSeq[ch71.Par.Par[A]])ch71.Par.Par[IndexedS
def sequence[A](lp: List[Par[A]]): Par[List[A]] = { //| eq[A]]
map(sequenceBalanced(lp.toIndexedSeq)){_.toList}
}

有了sequence就可以从List[Par[A]]到Par[List[A]],实现parMap应该没问题了:

  def parMap[A,B](as: List[A])(f: A => B): Par[List[B]] = fork {
val lps = as.map{asyncF(f)}
sequence(lps)
} //> parMap: [A, B](as: List[A])(f: A => B)ch71.Par.Par[List[B]]
fork(parMap(List(,,,,)){ _ + })(es).get //> pool-1-thread-1
//| pool-1-thread-2
//| pool-1-thread-3
//| pool-1-thread-4
//| pool-1-thread-5
//| pool-1-thread-6
//| pool-1-thread-8
//| pool-1-thread-7
//| pool-1-thread-9
//| pool-1-thread-10
//| pool-1-thread-14
//| pool-1-thread-12
//| pool-1-thread-15
//| pool-1-thread-11
//| pool-1-thread-13
//| res3: List[Int] = List(11, 12, 13, 14, 15)

现在我们的并行计算组件库已经能够提供一些基本的并行运算功能了。

泛函编程(19)-泛函库设计-Parallelism In Action的更多相关文章

  1. 泛函编程(30)-泛函IO:Free Monad-Monad生产线

    在上节我们介绍了Trampoline.它主要是为了解决堆栈溢出(StackOverflow)错误而设计的.Trampoline类型是一种数据结构,它的设计思路是以heap换stack:对应传统递归算法 ...

  2. 泛函编程(5)-数据结构(Functional Data Structures)

    编程即是编制对数据进行运算的过程.特殊的运算必须用特定的数据结构来支持有效运算.如果没有数据结构的支持,我们就只能为每条数据申明一个内存地址了,然后使用这些地址来操作这些数据,也就是我们熟悉的申明变量 ...

  3. 泛函编程(20)-泛函库设计-Further Into Parallelism

    上两节我们建了一个并行运算组件库,实现了一些基本的并行运算功能.到现在这个阶段,编写并行运算函数已经可以和数学代数解题相近了:我们了解了问题需求,然后从类型匹配入手逐步产生题解.下面我们再多做几个练习 ...

  4. 泛函编程(28)-粗俗浅解:Functor, Applicative, Monad

    经过了一段时间的泛函编程讨论,始终没能实实在在的明确到底泛函编程有什么区别和特点:我是指在现实编程的情况下所谓的泛函编程到底如何特别.我们已经习惯了传统的行令式编程(imperative progra ...

  5. 泛函编程(27)-泛函编程模式-Monad Transformer

    经过了一段时间的学习,我们了解了一系列泛函数据类型.我们知道,在所有编程语言中,数据类型是支持软件编程的基础.同样,泛函数据类型Foldable,Monoid,Functor,Applicative, ...

  6. 泛函编程(25)-泛函数据类型-Monad-Applicative

    上两期我们讨论了Monad.我们说Monad是个最有概括性(抽象性)的泛函数据类型,它可以覆盖绝大多数数据类型.任何数据类型只要能实现flatMap+unit这组Monad最基本组件函数就可以变成Mo ...

  7. 泛函编程(24)-泛函数据类型-Monad, monadic programming

    在上一节我们介绍了Monad.我们知道Monad是一个高度概括的抽象模型.好像创造Monad的目的是为了抽取各种数据类型的共性组件函数汇集成一套组件库从而避免重复编码.这些能对什么是Monad提供一个 ...

  8. 泛函编程(23)-泛函数据类型-Monad

    简单来说:Monad就是泛函编程中最概括通用的数据模型(高阶数据类型).它不但涵盖了所有基础类型(primitive types)的泛函行为及操作,而且任何高阶类或者自定义类一旦具备Monad特性就可 ...

  9. 泛函编程(9)-异常处理-Option

    Option是一种新的数据类型.形象的来描述:Option就是一种特殊的List,都是把数据放在一个管子里:然后在管子内部对数据进行各种操作.所以Option的数据操作与List很相似.不同的是Opt ...

  10. 泛函编程(6)-数据结构-List基础

    List是一种最普通的泛函数据结构,比较直观,有良好的示范基础.List就像一个管子,里面可以装载一长条任何类型的东西.如需要对管子里的东西进行处理,则必须在管子内按直线顺序一个一个的来,这符合泛函编 ...

随机推荐

  1. (转)数字格式化函数:Highcharts.numberFormat()

    一.函数说明 该函数用于图表中数值的格式化,常见用途有数值精度控制.小数点符.千位符显示控制等.   二.函数使用   1.函数构造及参数 Highcharts.numberFormat (Numbe ...

  2. 循环a数组(值代表b的下标)删除b数组中存在的记录,从后往前删

    for (var j = adelete.Count-1; j >= 0; --j)                     {                         aAttachm ...

  3. HTML5新特性之Web Worker

    1.概述 JavaScript语言采用的是单线程模型,也就是说,所有任务排成一个队列,一次只能做一件事.随着电脑计算能力的增强,这一点带来很大的不便,无法充分发挥JavaScript的潜能.龙其考虑到 ...

  4. EPLAN部件库之共享方法

    在使用EPLAN时经常会碰到自己电脑里的部件库和公司里其他同事的部件库存在差异,如果不是很平凡的同步所有使用的部件库,这种现象是不可避免的.这种情况对于一个团队用户来说是很麻烦的已经事,给维护部件库也 ...

  5. 正则表达式工具RegexBuddy使用教程(原创自Zjmainstay)

    从<我眼里的正则表达式(入门)>这个文章,到<深入正则表达式应用>,再到后来专门创建的正则QQ群"专精正则表达式",到现在已经过去2年时间了.在文章和群里一 ...

  6. UML3

    在UML系统开发中有三个主要的模型: 功能模型: 从用户的角度展示系统的功能,包括用例图. 对象模型: 采用对象,属性,操作,关联等概念展示系统的结构和基础,包括类图. 动态模型: 展现系统的内部行为 ...

  7. 【软件分析与挖掘】Multiple kernel ensemble learning for software defect prediction

    摘要: 利用软件中的历史缺陷数据来建立分类器,进行软件缺陷的检测. 多核学习(Multiple kernel learning):把历史缺陷数据映射到高维特征空间,使得数据能够更好地表达: 集成学习( ...

  8. SEO优化之Title 和 Meta 标签

    对搜索引擎最友好(Search Engine Friendly)的网页是静态网页,但大部分内容丰富或互动型网站都不可避免采用到相关技术语言来实现内容管理和交互功能.SEO 思想指导下的技术支持,主要是 ...

  9. log4net各种Filter使用

    log4net里面的filter类常用的为: 1.DenyAllFilter 拒绝所用的日志输出 <filter type="log4net.Filter.LevelMatchFilt ...

  10. bash的一些小技巧

    1.从输入读入变量 eg:read -ep "input yes or no: " flag 用e选项表示编辑,可以使用backspace删除 2.数组 a.索引数组 declar ...