【scala函数式编程】纯函数式并行计算

1. 问题的引出

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)
    }

2. 并行计算的数据类型

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等待中间结果的问题。

3. 并行计算的组合

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)))(_ + _))(_ + _)
  1. 如果只是构建一个描述,这将是一个重量级(heavyweight)对象,因为包含了所有将要执行的操作树,无论使用什么样的数据结构取存储,都要占用比列表本身更多的空间
  2. 重量级对象heavyweight:本例中特指需要占用较大内存空间的对象,反复分配释放会消耗很多资源,通用的讲,消耗时间、内存、io、网络连接等资源较多的对象,都是重量级对象,解决方案是使用对象池、io管道、连接池进行改进,防止对象反复创建
  3. 轻量级对象lightweight:适合反复创建的对象,简单判断的依据:创建销毁对象的开销小于维护对象的开销

4. 并行计算的显性分流

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),否则将不会达到真正并行的效果,既然这样,何时进行求值?

  1. 若求值放在Par.fork中,必需知道如何创建线程和提交任务到线程池,所有调用fork的地方线程池必需被正确初始化且可被fork访问,这样map2对线程池进行操作会收到干扰
  2. 若求值放在Par.run中,Par.run对接受到的Par.fork标记过的线程进行并发求值,具体指导Par何时创建线程、提交任务到线程池

5. 并行计算的结果表现形式

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的副作用才会暴露出来
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鱼摆摆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值