a. 求和问题
def sum(ints:Seq[Int]): Int =
ints.foldLeft(0)((a,b) => a+b)
b.求和问题改进-分治问题、递归求和
def sum(ints: Seq[Int]): Int =
if(ints.size <= 1) ints.headOption.getOrElse(0)
else{
val (l, r) = ints.splitAt(ints.length / 2)
sum(l) + sum(r)
}
a. 引入并发数据类型,对求和过程进行并行计算
def unit[A](a : => A):Par[A] // 函数模板,用于并行化求值
def get[A](a: Par[A]): A // 用于从Par[A]中解析出A并返回结果
b. 分治问题,原求和问题的并行化
// 引入并行接口Par.get,对传入Par.get中的参数进行并行计算
def sum(ints: =>ints: IndexedSeq[Int]): Int =
if(ints.size <= 1) ints.headOption.getOrElse(0)
else{
val (l,r) = ints.splitAt(ints.length / 2)
val sumL:Par[Int] = Par.unit(sum(l))
val sumR:Par[Int] = Par.unit(sum(r))
Par.get(sumL) + Par.get(sumR) // get操作依赖sumL和sumR的结果,会陷入等待,这是副作用
}
// 根据引用透明,上述代码等价于
def sum(ints: =>ints: IndexedSeq[Int]): Int =
if(ints.size <= 1) ints.headOption.getOrElse(0)
else{
val (l,r) = ints.splitAt(ints.length / 2)
Par.get(Par.unit(sum(l))) + Par.get(Par.unit(sum(r))) // 根据”引用透明“,else中的最后一步操作可以等效为Par.get(sumL) + Par.get(sumR)
}
按照scala函数参数从左到右的顺序严格求值这一特性,Par.get(sumL)在调用get后才会对sumL求值,即get会等待sumL返回结果,这是副作用之一,Par.get(sumR)同理。异步计算必需解决get等待中间结果的问题。
a. 高阶函数(将函数作为传入参数)解决get等待的问题
// 由于Par.get并不能做到并行,因此采用函数式编程,使用Par.map2,将get中的加法操作作为函数传递给map2,但这仍然会存在问题
def sum(ints: IndexedSeq[Int]) : Par[Int] = // 为了避免get等待,直接剔除get操作,不用解析Par[Int]
if(ints.size <= 1)
Par.unit(ints.headOption.getOrElse(0)) // 由于返回值类型为Par[Int],使用unit封装if分支中的结果
else{
val (l, r) = ints.splitAt(ints.length / 2)
Par.map2(sum(l), sum(r))(_,_) => (_ + _) // 避免出现unit和get方法,改用高阶函数(函数作为入参)改写为函数式代码
}
b. 计算过程分析,以IndexedSeq(1,2,3,4)为例,scala函数参数严格从左到右计算:
sum(IndexedSeq(1,2,3,4))
map2(sum(IndexedSeq(1,2)), sum(IndexedSeq(3,4)))(_ + _)
map2(map2(sum(IndexedSeq(1)), sum(IndexedSeq(2)))(_ + _), sum(IndexedSeq(3,4)))(_ + _)
map2(map2(unit(1),unit(2))(_ + _), sum(IndexedSeq(3,4))(_ + _))
map2(IndexedSeq(3), sum(IndexedSeq(3,4))(_ + _))
...
由此可见,map2并没能进行我们需要的并行计算,因为仍然是从左到右依次计算的,右侧参数仍然在等待左侧函数计算结束,改进方法是让map2称为惰性lazy的,从而让运算中的+在最后计算
c. 我们期望的计算过程如下:
sum(IndexedSeq(1,2,3,4))
map2(sum(IndexedSeq(1,2)), sum(IndexedSeq(3,4)))(_ + _)
map2(map2(sum(IndexedSeq(1)), sum(IndexedSeq(2)))(_ + _), map2(sum(IndexedSeq(3)), sum(IndexedSeq(4)))(_ + _))(_ + _)
- 如果只是构建一个描述,这将是一个重量级(heavyweight)对象,因为包含了所有将要执行的操作树,无论使用什么样的数据结构取存储,都要占用比列表本身更多的空间
- 重量级对象heavyweight:本例中特指需要占用较大内存空间的对象,反复分配释放会消耗很多资源,通用的讲,消耗时间、内存、io、网络连接等资源较多的对象,都是重量级对象,解决方案是使用对象池、io管道、连接池进行改进,防止对象反复创建
- 轻量级对象lightweight:适合反复创建的对象,简单判断的依据:创建销毁对象的开销小于维护对象的开销
a. 并行计算需要在必要的情况下进行分流,进行直接计算,本例中,并行计算需要分流的情形:Par.map2(Par.unit(1), Par.unit(2))(_ + _)
// 并行计算接口:Par.map2,用于合并多个并行计算任务
// 并行计算接口:Par.fork,用于产生一个独立的并行计算任务
// 直接计算接口:Par.unit,用于产生一个直接计算任务
def sum(ints: IndexedSeq[Int]): Par[Int] =
if(ints.length <= 1) Par.unit(ints.headOption.getOrElse(0))
else{
val (l,r) = ints.splitAt(ints.length / 2)
Par.map2(Par.fork(sum(l)), Par.fork(sum(r)))(_ + _)
}
b. map2必须惰性求值,惰性求值(lazy)的对立面是严格求值(strict),否则将不会达到真正并行的效果,既然这样,何时进行求值?
- 若求值放在Par.fork中,必需知道如何创建线程和提交任务到线程池,所有调用fork的地方线程池必需被正确初始化且可被fork访问,这样map2对线程池进行操作会收到干扰
- 若求值放在Par.run中,Par.run对接受到的Par.fork标记过的线程进行并发求值,具体指导Par何时创建线程、提交任务到线程池
a. 接口设计思路
def unit[A](a:A):Par[A]创建一个结果为a的并行计算
def map2[A,B,C](a:Par[A],b:Par[B])(f: (A,B) => C):Par[C]合并两个并行计算成为一个并行计算
def fork[A](a: => Par[A]):Par[A]要并发的计算,不会进行求值,直到run时才进行求值
def lazyUnit[A](a: => A):Par[A] = fork(unit(a))包装一个并发的不求值计算
def run[A](a: Par[A]): A对fork标记的Par进行并发求值,返回计算结果
b. Par所需的线程池:借助异步接口java.util.concurrent.ExecutorService
class ExectorService{
def submit[A](a: Callable[A]):Future[A]
}
trait Callable[A]{def call: A} // 惰性求值
trait Future[A]{
def get: A
def get(timeout: Long, unit: TimeUnit): A
def cancel(eventIfRunning:Boolean): Boolean
def isDone: Boolean
def isCancelled: Boolean
}
c. Par的具体实现
object Par{
def unit[A](a:A):Par[A] = (es:ExcutorService) => UnitFuture(a)
// unit表现为一个返回UnitFuture的函数,UnitFuture是一个包装了常量的Future的简单实现
// unit并不调用ExecutorService,因此不能取消,只是简单的在get调用时返回其值
private case class UnitFuture[A](get: A) extends Future[A]{
def isDone = true
def get(timeout:Long, units: TimeUnit) = get
def isCancelled = false
def cancel(eventIfRunning: Boolean): Boolean = false
}
def map2[A,B,C](a:Par[A],b:Par[B])(f: (A,B) => C):Par[C] = (es: ExecutorService) => {
val af = a(es)
val bf = b(es)
UnitFuture(f(af.get, bf.get))
}
// map2实现不考虑超时的问题,只是简单的将ExecutorService传递给两个Par,并等待af和bf的结果,再应用f,最终合并为一个UnitFuture
def fork[A](a: => Par[A]):Par[A] = es => es.submit(new Callable[A]{def call = a(es).get})
// 最简单的实现,但是问题在于外部的Callable会阻塞直到内部的任务完成。阻塞会导致线程池的线程被占用,降低并行度,相当于一个线程能够完成的事情,我们使用了两个线程
// Future没有一个纯粹的功能接口,Future的方法具有副作用(new Callable新建了对象)
// Par的API没有副作用,在用户调用run并传入ExcutorService时,Future的副作用才会暴露出来
}