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

 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. Rxlifecycle(一):使用

    Rxlifecycle使用非常方便简单,如下: 1.集成 build.gradle添加 //Rxlifecycle compile 'com.trello:rxlifecycle:0.3.1' com ...

  2. 部署tomcat在windows服务器下,将tomcat控制台日志记录到日志文件中

    在Linux系统中,Tomcat 启动后默认将很多信息都写入到 catalina.out 文件中,我们可以通过tail  -f  catalina.out 来跟踪Tomcat 和相关应用运行的情况. ...

  3. JavaScript封装Ajax(类JQuery中$.ajax()方法)

    ajax.js (function(exports, document, undefined){ "use strict"; function Ajax(){ if(!(this ...

  4. Xamarin.Forms中的ListView的ItemTrapped事件与ItemSelected事件的区别

    今天对Xamarin.Forms中的ListView的两个事件(ItemTrapped和ItemSelected)做了小小的研究,发现有以下几点区别: 1.ItemTrapped事件会优先被触发. 2 ...

  5. 科谱,如何单机环境下合理的备份mssql2008数据库

    前言: 终于盼来了公司的自用服务器:1U.至强CPU 1.8G 4核.16G内存.500G硬盘 X 2 (RAID1);装了64位win2008,和64位mssql2008.仔细把玩了一天把新老业务系 ...

  6. Can't get WebApplicationContext object from ContextRegistry.GetContext(): Resource handler for the 'web' protocol is not defined

    I'm stucked in configuring my web.config file under a web forms project in order to get an instance ...

  7. Linux高级编程--08.线程概述

    线程 有的时候,我们需要在一个基础中同时运行多个控制流程.例如:一个图形界面的下载软件,在处理下载任务的同时,还必须响应界面的对任务的停止,删除等控制操作.这个时候就需要用到线程来实现并发操作. 和信 ...

  8. STL中的算法小结

    ()要运用STL的算法,首先必须包含头文件<algorithm>,某些STL算法用于数值处理,因此被定义于头文件<numeric> ()所有STL算法都被设计用来处理一个或多个 ...

  9. 菜鸟学Windows Phone 8开发(1)——创建第一个应用程序

    本系列文章来源MSDN的 面向完全新手的 Windows Phone 8 开发 主要是想通过翻译本系列文章来巩固下基础知识顺带学习下英语和练习下自己的毅力(因为打算每天翻译一篇,但是发现翻译这篇花费了 ...

  10. Innodb Read IO 相关参数源代码解析

    前言:最近在阅读Innodb IO相关部分的源代码.在阅读之前一直有个疑问,show global status 中有两个指标innodb_data_reads 和 innodb_data_read. ...