Scala新手指南中文版 -第九篇 (Promise和Future实践)

译者注:原文出处http://danielwestheide.com/blog/2013/01/16/the-neophytes-guide-to-scala-part-9-promises-and-futures-in-practice.html,翻译:Thomas

 

在前一篇译文中,我介绍了Future类型,它的内在逻辑,以及如何使用它来写出可读性强且可可组合的异步执行代码。在文章里,我也提到Future只是完整拼图的一部分:它是一种只读类型,让你可以以一种优雅的方式来引用将被计算出的结果并且处理异常。为了让你能够从Future中读取到计算好的值,还需要让负责计算的代码有办法把计算好的值存起来。在本文中,我就会来说明如何借助Promise类型来实现,并提供一个如何在实际代码中使用Future和Promise的指南。

 

Promises

在前篇关于Future的文章中,我们写过一组传递给Future的伙伴对象的apply方法的代码,并且导入了ExecutionContext作为默认执行上下文,它就神奇的异步执行了那些代码,并且返回包装在Future中的结果。

虽然这是一种简单易行的方法来构造一个Future,还是有另外一种方法来生成Future实例并最终以成功或失败结束。Future提供一个仅用于查询的接口,Promise作为伙伴类型让你通过将结果置入来完成一个Future。这仅可以被完成一次。一旦Promise完成了,它就不能够被修改。

Promise实例总是被关联到一个Future实例。试着在REPL里再一次调用Future的apply试试,你一定会注意到Future返回的是Promise类型:

import concurrent.Future
import concurrent.ExecutionContext.Implicits.global
val f: Future[String] = Future { "Hello world!" }
// REPL output: 
// f: scala.concurrent.Future[String] = scala.concurrent.impl.Promise$DefaultPromise@793e6657

你得到的对象是一个DefaultPromise,它同时实现了Future和Promise。当然,Future和Promise可以被分别实现的。

上面的小小的例子表明除了通过Promise,显然没有其他途径完成一个Future - Future的apply方法仅仅作为助手方法方便你实现。

现在让我们来看看如何直接用Promise类型来做。

承诺一个光明的未来

 

当讨论承诺能否会被兑现时,生活中有一个明显的例子,关于政治家,选举,竞选宣言及立法过程。

假设获胜的政治家承诺过减税,这可以用一个Promise[TaxCut]来表示,通过调用Promise的伙伴对象的apply来生成,如下面的例子:

import concurrent.Promise
case class TaxCut(reduction: Int)
// 在apply方法中提供类型参数:
val taxcut = Promise[TaxCut]()
// 或者明确常量的类型,以便让Scala的类型推断系统能工作:
val taxcut2: Promise[TaxCut] = Promise()

 

一旦你生成了Promise,你可以通过调用Promise实例的future方法获得属于它的Future:

val taxcutF: Future[TaxCut] = taxcut.future

 

返回的Future可能和Promise不是同一对象,但是多次调用Promise的future方法确定无疑的总是返回同样的对象,这维持了Promise和Future的一对一关系。

完成一个Promise

一旦你做出了一个Promise并且告诉世人你将在可见的Future来达成,你最好尽其所能确保能实现诺言。

在Scala里,你可以成功或失败的完成一个Promise。

交付你的Promise

要成功地完成一个Promise,你调用它的success方法并传递一个结果值,值是对应的Future应该拥有的:

taxcut.success(TaxCut(20))

 

一旦你这样做了,Promise实例就会变成只读,任何试图写的操作都会抛出异常。

并且这样的方式完成Promise也会同时让相关联的Future成功完成。任何成功或完成的处理器都将被调用,或者当你在map那个Future时,map方法将被执行。

通常,完成Promise和完成Future的操作不会发生在同一个线程上。更多的场景是你生成了Promise并且在另一个线程开始进行结果的计算,立刻返回尚未完成的Future给调用者。

为了演示,我们来拿减税承诺举例:

object Government {
  def redeemCampaignPledge(): Future[TaxCut] = {
    val p = Promise[TaxCut]()
    Future {
      println("Starting the new legislative period.")
      Thread.sleep(2000)
      p.success(TaxCut(20))
      println("We reduced the taxes! You must reelect us!!!!1111")
    }
    p.future
  }
}

 

请不要被例子里的Future的apply方法的用法而困扰。我这样用它只是因为它让我很方便的异步执行一段代码。我也可以在一个Runnable里实现计算过程(包含了sleep的那段代码),并让Runnable异步的跑在ExecutorService,当然代码会冗余一些。这里的重点是Promise不再调用者线程里完成。

让我们来兑现竞选承诺并且为Future注册一个onComplete的回调函数:

 

import scala.util.{Success, Failure}
val taxCutF: Future[TaxCut] = Government.redeemCampaignPledge()
  println("Now that they're elected, let's see if they remember their promises...")
  taxCutF.onComplete {
    case Success(TaxCut(reduction)) =>
      println(s"A miracle! They really cut our taxes by $reduction percentage points!")
    case Failure(ex) =>
      println(s"They broke their promises! Again! Because of a ${ex.getMessage}")
  }
 

 

如果你执行这段代码几次,你会发现屏幕的输出是不可预测的。最终完成的处理器会被执行并且命中success的分支。

像个绅士一样违反承诺

作为一个政治家,你大部分情况下是不会遵守承诺的。作为Scala开发者,你有时候也没有其它选择。如果真的有不幸发生了,你仍然可以通过调用failure方法并传递一个异常给它来有好的结束Promise:

 

case class LameExcuse(msg: String) extends Exception(msg)
object Government {
  def redeemCampaignPledge(): Future[TaxCut] = {
       val p = Promise[TaxCut]()
       Future {
         println("Starting the new legislative period.")
         Thread.sleep(2000)
         p.failure(LameExcuse("global economy crisis"))
         println("We didn't fulfill our promises, but surely they'll understand.")
       }
       p.future
     }
}
 

 

redeemCampaignPledge()将会最终违反承诺。一旦你通过调用failure来完成了一个Promise,它就会变得不可写,和调用success的情形一样。相关联的Future现在也会以Failure结束,因此上面的回调函数将会进入failure的场景。

如果你的结算结果是Try类型,你可以通过调用complete来完成一个Promise,如果Try是一个Success,那么相关联的Future将会成功完成,并包含了Success里的值,如果Try是个Failure,Future将会以失败完成。

基于Future的编程实践

如果你为了提高应用的可扩展性而采用基于Future的架构,你必须设计你的程序从下至上都为非阻塞,之基本上意味着你应用的所有层级的函数都要为异步且返回Future。

现如今一个很好地场景就是开发web应用。如果你采用了现代的Scala Web框架,它会让你返回类似Future[Response]类型的响应而不用阻塞直到返回完成了的Response。这很重要因为它让你的web服务器以相对较少的线程来处理巨量的连接。通过确保使用Future[Response],你可以最大效率的使用web服务器的线程池资源。

最后,你的应用中的一些服务可能会多次访问数据库层以及/或者一些外部的webservice,接收到一些Future,然后将这些Future组合并返回一个新的Future,所有这些都在一个可读性很好地for语句里实现,就像你在前篇文章中看到的一样。web层再将这Future转化成Future[Response]。

那么在实践中你究竟应该如何来实现呢?下面有三个不同的场景必须考虑:

非阻塞IO

你的应用基本上一定会涉及到很多的IO操作。例如,访问数据库,或者作为客户端访问别的webservice。

只要有可能,就应该使用基于Java 非阻塞IO实现的函数库,可以是直接采用Java的NIO API的或者是通过类似Netty来实现的。这样的函数库也能以有限数量的线程池实现大量的访问连接。

自己开发类似的函数库是少数几个有理由直接使用Promise类型的地方之一。

阻塞IO

有时候没有基于NIO的函数库可用。例如,目前在Java世界大多数数据库驱动还是使用的阻塞式IO。如果你在响应一个HTTP请求的过程需要通过这样的驱动来查询数据库,这样的调用会在web服务器的线程中执行。为了避免那样做,将和数据库打交道的代码封装在一个Future块中,类似:

// get back a Future[ResultSet] or something similar:
Future {
  queryDB(query)
}

 

到目前为止,我们一直在用隐式的全局ExecutionContext来执行类似的Future代码块。也许为这样的数据库访问创建一个专用的ExecutionContext是一个不错的想法。

你可以从Java的ExecutorService中创建一个ExecutionContext,这意味着你可以为这个线程池做一些特定的优化和调优而不影响其他线程池:

import java.util.concurrent.Executors
import concurrent.ExecutionContext
val executorService = Executors.newFixedThreadPool(4)
val executionContext = ExecutionContext.fromExecutorService(executorService)

长周期的计算

有赖于你的应用的特性,也许会存在执行时间比较久的计算任务,它们完全没有IO请求而是CPU消耗型。这些计算也不应该在web服务器的线程池里执行。因此你也应该将它们置入Future:

Future {
  longRunningComputation(data, moreData)
}

同样的,如果存在执行时间久的计算,为它们创建一个单独的ExecutionContext也是个好主意。如何调优不同的线程池是高度依赖于不同应用特点的,也不是本文的所要讨论的范畴。

总结

在本篇中,我们探索了Promise,它是基于Future的并行架构的可写的部分,讨论了如何用Promise来完成一个Future,最后讲到实践中如何使用Future。

在下一篇中,我们会回头再看看并发的问题并且检验Scala的函数式编程如何帮助你写出可读性更好地代码。

作者:Daniel Westheide,2013/1/16

你可能感兴趣的:(Scala,Scala,Functional,Programming)