最近看了《七周七语言:理解多种编程泛型》,介绍了七种语言(四种编程泛型)的主要特性:基本语法,集合,并行/并发,其中就有 Scala。你不能指望这种书全面介绍,因为其中任何一门语言都够写一本书了~
我比较关注并行/并发,但是书中关于 Scala 的并发部分——Actor,代码编译不通过,“Deprecated”,哎,这书点不负责,代码也不写采用编译器的版本。于是就到 Scala 官网看了一下,即便是官网,也列出了对 Actor 的改进,有些已经不再使用了~
Java 在它的版本 8 之前,函数式编程实在太弱了,不然也不会出现像 Scala 这样在 JVM 上运行,能够与 Java 完美融合的语言(估计,Java 在函数式编程在这方面,太落后了,社区已经等不急了,而函数式编程最大的优点是——并行)。
Future提供了一套高效非阻塞(non-blocking)的方式完成并行操作。其基本思想很简单,所谓 Future,指的是一类占位符对象(placeholder object),用于指代某些尚未完成计算的结果。一般,由Future的计算结果都是并行执行的,计算完后再使用。以这种方式组织并行任务,便可以写出高效、异步、非阻塞的并行代码。
默认情况,future 和 promise 利用回调(callback)的非阻塞方式,并不是采用典型的阻塞方式。为了在语法和概念层面简化回调的使用,Scala 提供了 flatMap、foreach 和 filter 等算子(combinator),使得我们能够以非阻塞的方式对future进行组合。当然,future 对于那些必须使用阻塞的情况仍然支持阻塞操作,可以阻塞等待future(不过不鼓励这样做)。
一个典型的 future 如下所示:
val inverseFuture:Future[Matrix]=Future{ fatMatrix.inverse()// non-blocking long lasting computatio }(executionContext)
implicit val ec:ExecutionContext=... val inverseFuture :Future[Matrix]=Future{ fatMatrix.inverse() }// ec is implicitly passed
inverseFuture
中体现计算结果。
所谓 Future,是一种用于指代某个尚未就绪的值的对象。这个值通常是某个计算过程的结果:
Future 完成分为两种情况:
Future 具有一个重要的属性——只能被赋值一次。一旦给定了某个值或某个异常,future对象就变成了不可变对象——无法再被改写。
创建future对象最简单的方法是调用future方法,开始异步(asynchronous)计算,并返回保存有计算结果的futrue。一旦该future计算完成,其结果就变的可用。
注意,Future[T] 是一个类型,表示future对象,而future是一个方法,创建和调度一个异步计算,并返回一个带有计算结果的future对象。
下面通过一个例子来展示。
假设,我们使用某个社交网络假想的API获取某个用户的朋友列表,我们将打开一个新对话(session),然后发送一个获取特定用户朋友列表的请求。
import scala.concurrent._ importExecutionContext.Implicits.global val session = socialNetwork.createSessionFor("user", credentials) val f:Future[List[Friend]]=Future{ session.getFriends() }
上面,首先导入 scala.concurrent 包。然后,通过一个假想的 createSessionFor 方法初始化一个向服务器发送请求 session 变量。这个请求是通过网络发送的,所以可能耗时很长。调用 getFriends 方法返回 List[Friend]。为了更好的利用CPU,知道响应到达,不应该阻塞(block)程序的其他部分,这个计算应该被异步调度。future方法就是这样做的,它并行地执行指定的计算块,在这个例子中,向服务器发送请求,等待响应。
一旦服务器响应,future f 中的好友列表将变得可用。
失败可能会导致一个 exception。在下面的例子中,session 的值未被正确的初始化,于是,future 块中计算将抛出一个 NullPointerException。这样,future f 失败了。
val session =null val f:Future[List[Friend]]=Future{ session.getFriends }
上面的 import ExecutionContext.Implicits.global
导入默认的全局执行上下文(global execution context)。执行上下文执行提交给他们的任务,你也可把执行上下文看作线程池,这对future方法是必不可少的,因为,它们处理如何和何时执行异步计算。你可以定义自己的执行上下文,并用 future 使用,但现在,只需要知道你能够通过上面的语句导入默认执行上下文就足够了。
我们的例子是基于一个假想的社交网络 API,计算包含了发送网络请求和等待响应。下面,假设你有一个文本文件,想找出一个特定词第一次出现的位置。当磁盘正在检索此文件时,这个计算过程可能会陷入阻塞,因此,并行执行程序的剩余部分将很有意义。
val firstOccurrence:Future[Int]=Future{ val source = scala.io.Source.fromFile("e:scalamyText.txt") source.toSeq.indexOfSlice("myKeyword") }
现在,我们知道如何开始一个异步计算来创建一个新的future值,但是我们没有演示一旦此结果变得可用后如何使用。我们经常对计算结果感兴趣而不仅仅是它的副作用(side-effects)。
在许多future的实现中,一旦future的客户端对结果感兴趣,它必须阻塞它自己的计算,并等待直到future完成——然后才能使用future的值继续它自己的计算。虽然这在Scala Future API(在后面会展示)中是允许的,但从性能角度来看更好的办法是完全非阻塞,即在future中注册一个回调。一旦future完成,就异步调用回调。如果当注册回调,future已经完成,那么,回调或是异步执行,或在相同的线程中循序执行。
注册回调最通常的形式,是使用OnComplete方法,即创建一个Try[T] => U
类型的回调函数。如果future成功完成,回调则会应用到Success[T]类型的值中,否则应用到 Failure[T]
类型的值中。
Try[T]
跟 Option[T]
或 Either[T, S]
相似,因为它是一个可能持有某种类型值的单子(monda)。然而,它是为持有一个值或异常对象特殊设计的。Option[T]
既可以是一个值(如:Some[T]
)也可以完全不是值(如:None
),如果Try[T]
获得一个值是,那么它是Success[T]
,否则为持有异常的 Failure[T]
。 Failure[T]
有很多信息,不仅仅是关于为什么没有值 None。同时,也可以把Try[T]
看作一种特殊版本的 Either[Throwable, T]
,特别是当左边值为一个 Throwable 的情形。
“一个单子(Monad)说白了不过就是自函子范畴上的一个幺半群而已。”这句话出自Haskell大神Philip Wadler,也是他提议把Monad引入Haskell。
回到我们社交网络的例子,假设,我们想获取最近的帖子并显示在屏幕上,可以通过调用 getRecentPosts 方法,它返回 List[String]:
import scala.util.{Success,Failure} val f:Future[List[String]]=Future{ session.getRecentPosts } f onComplete { caseSuccess(posts)=>for(post <- posts) println(post) caseFailure(t)=> println("An error has occured: "+ t.getMessage) } onComplete 方法允许客户处理失败或成功的future 结果。对于成功,onSuccess 回调使用如下: val f:Future[List[String]]=Future{ session.getRecentPosts } f onSuccess { case posts =>for(post <- posts ) println(post) } 对于失败,onFailure 回调使用如下: val f:Future[List[String]]=Future{ session.getRecentPosts } f onFailure { case t => println("An error has occured: "+ t.getMessage) } f onSuccess { case posts =>for(post <- posts) println(post) }
onFailure 回调只有在 future 失败,也就是包含一个异常时才会执行。
因为部分函数(partial functions)具有 isDefinedAt 方法, 所以,onFailure
方法只有为了特定 Throwable 而定义才会触发。下面的例子,已注册的onFailure
回调永远不会被触发:
val f =Future{ 2/0 } f onFailure { case npe:NullPointerException=> println("I'd be amazed if this printed out.") }
回到前面例子,查找某个第一次出现的关键字,在屏幕上输出该关键字的位置:
val firstOccurrence:Future[Int]=Future{ val source = scala.io.Source.fromFile("myText.txt") source.toSeq.indexOfSlice("myKeyword") } firstOccurrence onSuccess { case idx => println("The keyword first appears at position: "+ idx) } firstOccurrence onFailure { case t => println("Could not process file: "+ t.getMessage) }
onComplete,、onSuccess 和 onFailure 方法都具有结果类型 Unit,这意味着这些回调方法不能被链接。注意,这种设计是为了避免链式调用可能隐含在已注册回调上一个顺序的执行(同一个 future 中注册的回调是无序的)。
也就是说,我们现在应讨论论何时调用回调。因为回调需要future 中的值是可用的,只有future完成后才能被调用。然而,不能保证被完成 future 的线程或创建回调的线程调用。反而, 回调有时会在future对象完成后被某个线程调用。我们可以说,回调最终会被执行。
更进一步,回调被执行的顺序不是预先定义的,甚至在同一个应用程序。事实上,回调也许不是一个接一个连续调用的,但在同一时间并发调用。这意味着,下面例子中,变量 totalA 也许不能从计算的文本中得到大小写字母数量的正确值。
@volatilevar totalA =0 val text =Future{ "na"*16+"BATMAN!!!" } text onSuccess { case txt => totalA += txt.count(_ =='a') }