`
thomassun
  • 浏览: 27546 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

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中进行编程:全面的分步指南Programming in Scala: A comprehensive step-by-step guide

    总结以上信息,可以认为这本书是一份宝贵的资源,不仅对Scala新手,也对希望深入了解并有效利用这门语言的中级或高级开发人员都非常有价值。从编程基础到高级特性,再到最佳实践,本书均可能提供全面的指导和讲解。

    scala-intellij-bin-2020.1.43

    4. 文档:可能包括API参考、用户指南或开发文档,帮助开发者更好地理解和使用Scala和IntelliJ IDEA插件。 5. 更新日志或发行说明:这些文件会详细列出该版本的改进和修复的问题。 总的来说,"scala-intellij-bin-...

    scala-intellij-bin-2018.3.5.zip

    Scala是一种强大的多范式编程语言,它融合了面向对象和函数式编程的概念。IntelliJ IDEA是一款广受...对于新手,这个插件极大地简化了学习和使用Scala的过程,而对于经验丰富的Scala开发者,它则提供了高效的开发工具。

    Scala in Action.pdf

    通过丰富的示例和实用的技术指导,读者可以逐步掌握Scala的核心概念和最佳实践。无论是对于想要提高编程技能的专业人士,还是对于那些希望通过Scala解决实际问题的人来说,这本书都是一本不可或缺的资源。

    scala 教程

    - **标题**:“Scala 教程”明确指出这是一份关于Scala编程语言的学习指南。 - **描述**:“Scala 教程 .pdf”进一步强调这份教程是以PDF格式提供的。 **标签解析:** - “Scala 教程 scala 教程”重复强调该文档是...

    Scala for the Impatient 完整版带书签

    ### Scala for the Impatient:快速学习Scala的指南 #### 一、书籍简介 《Scala for the Impatient》是一本由Cay S. Horstmann所著的关于Scala编程语言的入门书籍,它被誉为是快速学习Scala的最佳选择之一,并且...

    Programming.in.Scala.2nd.Edition

    ### 《Programming in ...无论您是Scala新手还是有一定经验的开发者,本书都能够为您提供有价值的指导和启发。通过阅读本书,您可以深入了解Scala语言的强大功能,并将其应用到实际项目中,提高软件开发的质量和效率。

    The Neophyte's Guide to Scala

    《Scala新手指南》是一本旨在帮助对Scala和函数式编程感兴趣,但对目前入门资源中的问题感到困惑,以及对高级资源中过多技术性概念感到难以理解的读者,深入了解Scala和函数式编程的书籍。本书的目标读者是那些已经...

    Programming in Scala, 2nd Edition

    《Programming in Scala, 2nd Edition》是一本专注于Scala编程语言的综合指南书籍,该书的第二版针对Scala 2.8版本进行了更新。本书是由Artima出版社出版,主要作者包括Martin Odersky、Lex Spoon以及Bill Venners,...

    Artima.Actors.in.Scala.Mar.2011

    Philipp Haller不仅是本书的作者之一,同时也是Scala Actor框架的主要创造者和维护者,这使得本书成为了理解和掌握Scala Actor框架的权威指南。 #### 二、Actor模型简介 **Actor模型**是一种用于构建并发系统的设计...

    Atomic Scala

    最后,书中包含了两个附录,分别是“AtomicTest”和“从Java调用Scala”,它们为读者提供了额外的编程示例和实践。此外,书中还提供了一个详尽的索引,便于查找特定概念或话题。 由于书中涉及到的Scala知识点极为...

    Scala.for.the.Impatient.2nd.Edition.2016.12

    这本书的第二版在2016年12月出版,旨在帮助编程新手迅速理解Scala的核心概念和特性,从而能够在实际项目中高效地使用这门语言。 Scala是一种多范式编程语言,它结合了面向对象和函数式编程的思想。该书首先会介绍...

    play-scala-seed

    这个项目种子是新手学习Scala和Play框架的良好起点,也是经验丰富的开发者快速启动新项目的一个便捷工具。 在深入理解这个项目之前,我们先来了解一下关键组成部分: 1. **Scala**:Scala是一种多范式编程语言,...

    leetcode2-leetcode:用于Scala的leetcode

    总结来说,"leetcode2-leetcode:用于Scala的leetcode"是一个集学习、实践和分享为一体的项目,无论你是Scala新手还是经验丰富的开发者,都能从中受益。通过探索"leetcode-master"目录下的代码,你将深化对Scala的...

    play-scala-starter-example:播放Scala入门模板(适合新用户!)

    )" 提供了一个基于Scala和Play Framework的初始项目结构,是初学者踏入Scala Web开发的理想起点。Play Framework是一个开源的Web应用程序框架,它采用模型-视图-控制器(MVC)架构模式,以异步、事件驱动的方式处理...

    IntelliJ IDEA 中文指南.pdf

    《IntelliJ IDEA 中文指南》是一份综合性的资源,它以新手用户为出发点,逐步深入到IntelliJ IDEA的高级功能和最佳实践。文档不仅涵盖了安装配置、基础操作,还深入到了插件应用、快捷键操作、项目管理以及DevOps...

    Spark开发指南.pdf

    ### Spark开发指南知识点详解 #### 一、Spark简介与特性 **Spark** 是一个高度优化且功能丰富的集群计算框架,其核心优势在于基于内存的数据处理能力,这使得它相较于传统的 MapReduce 框架有着显著的性能提升。...

    kafka-manager-1.3.3.15.zip(已编译)

    《Kafka-Manager 1.3.3.15 编译详解与应用指南》 Kafka-Manager 是一个基于 Apache Kafka 的管理工具,由 Yelp 开发并开源,旨在简化 Kafka 集群的管理和监控。这个压缩包 "kafka-manager-1.3.3.15.zip" 包含了 ...

Global site tag (gtag.js) - Google Analytics