Scala + Future 实现异步编程

3,250 阅读13分钟

多核处理器以及并行任务的逐渐普及,人们对异步编程也越来越关注。Scala 标准库中提供的 Future 允许你在得到真正的执行结果之前,就允许通过 mapfilter 等集合操作得到下一个变换之后的异步结果。

我们无需再以阻塞的方式等待每一步结果,而是使用 Future 快速构造出一个异步的,对一系列不可变的结果的操作流水线出来。

早在笔者之前介绍的 Akka ,Play 等实战 Demo 中,我们就已经接触过 Future 了。在本专题中,将详细地介绍如何正确地使用它。

坎坷的线程同步控制

Java 为每一个对象关联了逻辑监视器( monitor ),用来控制对数据的多线程访问。通过这种模型,我们来决定哪些数据可以被多线程共享,并使用 synchronized 关键字 “加锁” 。

要用锁模型来创建一个健壮的多线程应用实际上是一件非常困难的事情。对于每个需要被共享的数据,我们都要为它上锁,并且确保不会引发死锁问题。然而,即便我们主动对数据上锁,它们也不是在编译期间就固定的,在运行期间,程序仍然可以任意地创建新的锁。

后来, Java 提供了 java.util.concurrent 包来提供更高级别的抽象同步,至少要比自己手动通过各种同步语法来实现不稳定的同步机制要来得快。然而,这类工具包仍然是基于共享数据和锁的,因此在本质上没有解决该类模型的种种困难。

在 Scala 程序中创建第一个 Future

Scala Future 在相当程度上减少了程序员对共享数据进行和锁处理的负担。如果某个函数的执行结果返回的是一个 Future ,则它意味着将返回另一个要被异步执行的计算,而至于哪个线程来处理这个稍后的异步计算,将由 Scala 提供的 执行上下文( Execution Context ) 来决定。

因此,在使用 Future 实现异步编程之前,首先需要将执行上下文导入进来:

import scala.concurrent.ExecutionContext.Implicits.global

这几乎是一个必选项,否则,程序在编译时将会报错。我们通过 Future 伴生对象提供的 apply 方法创建第一份 “未来计划”:

val future = Future {
	// 在这个计划中,执行它的线程会首先休眠 3 秒,然后返回一个 Int 值。
	Thread.sleep(3000)
	200
}

有两种方法确定这个异步计算是否已经得到结果:

  1. 调用 future.isCompleted,如果异步计算还未执行完毕,则返回 false
  2. 调用 future.value,如果计算完毕,则返回 Some(Sussess(value)),否则返回 None

为什么 value 方法做了两层包裹呢?首先,需要考虑到这个异步计算是否执行完毕。因此最外层返回的是一个 Option 类型,如果有计算结果,则返回,否则 None

另外,计算结果也包含了两种情况。如果计算时没有出现错误,则计算结果可以装入 Success 类中返回。反之,调用 value 将返回一个 Failure

Try 类型

scala_try.png

SuccessFailure 属于 Try 类,代表着两个异步运算的两个可能结果。它的目的是提供一个在同步计算中类似于 try ... catch 的效果,允许程序员自行处理返回 Failure 的情况。

在异步编程当中,try/catch 语句将不再有效,因为 Future 计算经常都在别的线程当中执行,而导致原线程当中并不能捕捉到异常。此时,大写的 Try 类型就排上用场了:如果某个异步计算抛出了 Failure ,则说明这个计算过程当中出现了一些意外。

对 Future 进行流式处理

某个异步计算的 Future 可以通过 mapfilter 等操作衔接到另一个异步计算中。例如:

//后续的代码端中将不再提醒导入执行上下文。
import scala.concurrent.ExecutionContext.Implicits.global

val future1 : Future[Int] = Future {
  // 在这个计划中,执行它的线程会首先休眠 3 秒,然后返回一个 Int 值。
  Thread.sleep(3000)
  println("执行 future1")
  4
}

val future2: Future[Int] = future1.map(p => {
  //两秒后执行这个计算。
  Thread.sleep(2000)
  println("执行 future2")
  p + 5
})

第一个异步计算会在 3 秒后执行,并返回一个 Int 值类型。在理想情况下,当第一个异步计算执行完毕后,它的下一步将是把刚才的返回值进行加操作。这个流程又被命名为 future2 。显然,它的计算返回值仍然是一个 Future 类型。

对于主线程而言,它将在大约 5 秒之后得到一个结果 :9 。

使用 for 表达式对 Future 做变换

Scala 的 for 表达式功能要比 Java 强大得多,包括用于组合 Future 计算事件。

基于上述的异步运算 future1future2 ,我们创建第三个运算 future3,对刚才的两个运算结果进行加和。代码清单如下:

//后续的代码端中将不再提醒导入执行上下文。
import scala.concurrent.ExecutionContext.Implicits.global

//观察开始时间
println(new java.util.Date())

val future1 : Future[Int] = Future {
    // 在这个计划中,执行它的线程会首先休眠 3 秒,然后返回一个 Int 值。
    Thread.sleep(3000)
    println("执行 future1")
    4
}

val future2 : Future[Int] = Future {
    Thread.sleep(2000)
    println("执行 future2")
    5
}

val future3: Future[Int] = for {
    x <- future1
    y <- future2
} yield {
    x + y
}
//我们这里使用了 Await 等待结果调用完毕,不限制等待时间。
println(Await.result(future3,Duration.Inf))
//观察结束时间
println(new java.util.Date())

for 循环在底层实际上会将这段代码转换为串行化的 flatmap 句式:future1.faltMap( x => future2.map(y => x + y)) 。从主程序完成到完成计算,总共花费了 3 秒的时间(而不是 5 秒),因为上述的代码都是在异步的环境中执行完成的。

我们可以画出一个简单的 PETRI 图出来,并求出这个图的最短完成时间(详情参考离散数学科目:关键路径的相关知识)。

for_x+y.png

注意,如果使用 for 表达式对 Future 做变换,一定要将 Future 声明在 for 循环的前面,否则 for 表达式将在串行的环境下完成它们。

创建 Success,Failure

Future 提供诸多已经完成的 future 的工厂方法:successful, failed 以及 fromTry 。这些方法不需要手动导入上下文。

使用 successful 方法来创建一个已经完成的 future :

val future: Future[Int] = Future.successful({
    println("返回一个已经完成的 Success[T]")
    100
})

// Some(Success(100))
println(future.value)

使用 failed 方法创建一个已经完成,但是出现异常的 future:

val future: Future[Nothing] = Future.failed({
    println("该方法用于返回一个 Failure[T]")
    new Exception("Oops!")
})

//Some(Failure(java.lang.Exception: Oops!))
println(future.value)

如果不确定抛出 Try[+T] 的哪一种情况,则调用 fromTry

val future: Future[Nothing] = Future.fromTry({
    println("可能返回 Success 或者 Failure")
    //      Success(100)
    Failure(new Exception("Oops!"))
})

println(future.value)

两种等待方式

Await 同步等待

本文刚才所提到的 Await 是一种同步等待机制,主线程会在有限的时间内等待某个 Future 进行。

我们另引入一个包:scala.concurrent.duration._,这样就允许我们使用 2 second 这种方式来表示我们的最大等待时间了(笔者曾经在隐式转换章节中介绍过如何实现它)。

Await 主要有两个方法。第一个用法是调用 result 另主线程进入阻塞等待,直到获取该 future 的返回值。

val intFuture = Future {
    println("正在计算...")
    println("执行此计算任务的线程是:" + Thread.currentThread().getName)
    Thread.sleep(1000)
    30
}

//主程序会在 3 秒内等待该结果,并赋值。
val int : Int = Await.result(intFuture,3 second)
println(int)

一般用于需要获取到该 future 的返回值才能做进一步操作的情况,如果只关心该 future 的完成状态,可以调用 ready 方法。当 future 仍处于工作状态时,主线程会等待至多 3 秒。

Await.ready(intFuture, 3 second)

另外,通过 Thread.currentThread().getName 可以发现,此 future 是由另一个线程执行的:ForkJoinPool-X-worker-XX

onComplete 异步等待

忠告:如果你已经进入了 Future 空间内,就尽量不要再使用 Await 阻塞 future 的执行。Scala 提供注册 “回调函数” 的方式来令你通过函数副作用获取到某个 future 在未来返回的值。

val intFuture = Future {
    println("正在计算...")
    println("执行此计算任务的线程是:" + Thread.currentThread().getName)
    Thread.sleep(1000)
    30
}

//    Await.ready(intFuture, 3 second)
//    和刚才的情况不同,如果主线程不阻塞一会,那么这个程序会提前结束推出。
Thread.sleep(3000)

var intValue : Int = 0

intFuture onComplete {
    case Success(value) => 
      println(value)
      // 通过代码块副作用获取到这个 Future 的 value 返回值。
      intValue = value
    case _ => println("出现了意外的错误")
}

这种方式不会阻塞主线程,为了能看到程序运行结果,我们需要主动调用 Thread.sleep 让主线程休眠一会,否则程序会立刻结束。onComplete 的返回值是一个 Unit 数据类型。

使用 andThen 强制保证 future 的执行顺序

一个 future 可以绑定多个 onComplete 。然而,上下文环境并不会保证哪个 future 的 onComplete 会被率先触发,而 andThen 方法保证了回调函数的执行顺序。

import scala.concurrent.ExecutionContext.Implicits.global

val intFuture = Future {
    Thread.sleep(2000)
    println(Thread.currentThread().getName)
    200
}

// 主程序的 onComplete 方法的调用顺序不一定
intFuture onComplete {
    case Success(int) => println(s"this future returned $int")
    case _ => println("something wrong has happened.")
}

intFuture onComplete {
    case Success(int) => println(s"completed with the value of $int")
    case _ => println("something wrong has happened.")
}

Thread.sleep(3000)

执行上述的程序,控制台有可能先打印 this future returned $int ,也有可能先打印 completed with the value of $int

import scala.concurrent.ExecutionContext.Implicits.global

val intFuture = Future {
    Thread.sleep(2000)
    println(Thread.currentThread().getName)
    200
}

intFuture onComplete {
    case Success(int) => println(s"this future returned $int")
    case _ => println("something wrong has happened.")
}

intFuture andThen  {
    case Success(int) => println(s"completed with the value of $int")
    case _ => println("something wrong has happened.")
}

Thread.sleep(3000)

andThen 方法会返回原 future 的一个镜像,并且只会在该 future 调用完 onCompelete 方法之后,andThen 才会执行。

Promise

当我们不确定 future 何时会完成时,可以会借助 Promise 许下一个 “承诺” ,它表示:在某个未来的时间点,一定能够得到值。

val promisedInt: Promise[Int] = Promise[Int]

然而,这个 Int 值的计算实际上委托给了其它的 future 来完成。受托的 Future 在计算完结果之后会调用该 promise 的 success 方法来 “兑现” 这个承诺。

val intFuture = Future {
  println("正在计算...")
  println("执行此计算任务的线程是:" + Thread.currentThread().getName)
  Thread.sleep(1000)

  //一旦这样做,这个 promise 将和当前的 future 绑定。
  promisedInt.success(300)
}

考虑到异常情况,除了 success 方法, Promise 还提供了 failure , Complete 等方法。无论调用哪种方法,一个 Promise 都只能被使用一次

promisedInt.success(300)
// promisedInt.failure(new Exception("可能的错误"))
// promisedInt.complete(Success(1))

随后此 promise 的 future 会进入就绪状态,我们使用刚才介绍的 onComplete 回调函数中 "兑现" 它的返回值。

promisedInt.future onComplete {
    case Success(value) => println(value)
    case _ => println("出现了意外的错误")
}

PromisedInt 在这里充当着代理的作用。它承诺提供的值具体要由哪个 future 来计算并提供,程序的调用者可能并不关心:它也许是 intFuture ,也许是 IntFuture2 。因此,我们仅需要为代理( PromisedInt.future )设置回调函数,而不是其它的 future。为了方便理解,这里给出连贯的代码清单:

import scala.concurrent.ExecutionContext.Implicits.global

val promisedInt: Promise[Int] = Promise[Int]

val intFuture = Future {
  println("正在计算...")
  println("执行此计算任务的线程是:" + Thread.currentThread().getName)
  Thread.sleep(1000)

  // promisedInt 承诺的值由 intFuture 真正实现。
  promisedInt.success(300)
  promisedInt.failure(new Exception("可能的错误"))
  promisedInt.complete(Success(1))
}

//    和刚才的情况不同,如果主线程不阻塞一会,那么这个程序会提前结束退出。
Thread.sleep(3000)

//    主函数只关心 promisedInt 能否提供值。
promisedInt.future onComplete {
  case Success(value) => println(value)
  case _ => println("出现了意外的错误")
}

过滤 Future 的返回值

Scala 提供两种方式让你对 future 的返回值进行检查,或者过滤。filter 方法可以对 future 的结果进行检验。如果该值合法,就进行保留。下面的例子使用 filter 确保返回值是满足 >=30 的值。注意,执行 filter 方法之后得到的是另一个 future 值。

import scala.concurrent.ExecutionContext.Implicits.global

val eventualInt = Future {

    Thread.sleep(3000)
    print(s"${Thread.currentThread().getName} : return result.")
    12
}

// 检查返回值是否 >= 30 .
val checkRes: Future[Int] = eventualInt filter(_ >= 30)

// 阻塞等待
while (!checkRes.isCompleted){
    Thread.sleep(1000)
    println("waiting..")
}

// 注册回调。
checkRes onComplete {
    case Success(res) =>
      println(s"result : $res")
    case Failure(cause) =>
      println(s"failed because of $cause")
}

如果不满足匹配的要求,则它会返回 java.util.NoSuchElementException: Future.filter predicate is not satisfied 。你可以在 case Failure(casue) => 中捕获它。

Futurecollect 方法允许你使用偏函数对结果进行中间变换,可以使用 case 语句对偏函数进行缩写。

Thread.sleep(3000)
print(s"${Thread.currentThread().getName} : return result.")
22
}

// 检查返回值是否 >= 30 . 采取不同的策略。
val transformRes: Future[Int] = eventualInt collect {
  case res : Int if res > 30 => res + 30
  case res : Int if res > 20 => res + 20
}

while (!transformRes.isCompleted){
    Thread.sleep(1000)
    println("waiting...")
}

transformRes onComplete {
  case Success(int) => println(s"value of $int")
  case Failure(cause) => println(s"failed because of $cause")
}

处理失败的预期

failed 方法

Scala 提供了几种处理失败的 future 的方式:包含 failed , fallbackTo , recoverrecoverWith 。举例:如果某个 future 在执行时出现异常,则 failed 方法会返回一个成功的 Future[Throwable] 实例。

import scala.concurrent.ExecutionContext.Implicits.global

val intFuture = Future {10 / 0}

intFuture onComplete {
  case Success(int) => println(int)
  case Failure(cause) => println(s"failed because of $cause")
}

val eventualThrowable: Future[Throwable] = intFuture failed

//Some(Success(java.lang.ArithmeticException: /by zero))
println(eventualThrowable.value)

如果 future 是被正常执行的,则 failed 方法反而会抛出 NoSuchElement

fallbackTo 方法

fallbackTo 方法提供了保险机制,它允许原始的 future 失败时,转而去运行另一个 future2 。

val intFuture = Future {10 / 0}

intFuture onComplete {
    case Success(int) => println("intFuture"  + int)
    case Failure(cause) => println(s"failed because of $cause")
}

val maybeFailed: Future[Int] = intFuture fallbackTo Future {100}

maybeFailed onComplete {
    case Success(int) => println("maybeFailed" + int)
    case _ => println("This future's throwable will be ignored.")
}


Thread.sleep(2000)
println(maybeFailed.value)

无论 intFuture 执行是否成功,maybeFailed 也总是会运行(笔者亲测),因此不要在这里设置一些具有副作用的代码。当 intFuture 运行成功时,maybeFailed 的返回值将被会忽略,它实际返回的是 intFuture 的返回值。在 intFuture 运行失败的情况下, maybeFailed 方法的返回值才会生效。

如果 maybeFailed 在执行时也出现了异常,则它抛出的异常将被忽略,捕捉到的将是上一级 intFuture 的原始异常。

recover 方法

另一个方法 recoverfallbackTo 方法的逻辑类似:如果它捕获到了异常,则允许你根据异常的类型采取对应的策略并返回值。但是如果调用它的原始 future 执行成功,则这个备用值同样会被忽略。同理,传入 recover 的偏函数中如果没有对指定异常的处理,则原始 future 的异常会被透传。

import scala.concurrent.ExecutionContext.Implicits.global

val intFuture = Future {
    10 / 0
}

val eventualInt: Future[Int] = intFuture recover {
    case ex: ArithmeticException => 100
    case ex: Exception => 200
}

intFuture onComplete {
    case Success(int) => println(int)
    case Failure(cause) => println(s"failed because of $cause")
}

Thread.sleep(3000)
println(eventualInt.value)

Transform : Die or Live

future 的 Transform 方法接收两个函数对 future 进行变换,它们分别用于处理成功和失败。注意,在 Scala 2.12 版本之前,Transform 需要传入两个函数,因此笔者在这里没有使用偏函数形式的 case 语句。

Transform 方法得到的是另一个独立的,和原始 future 完全无关的另一个 future 。因此原始 future 的结果不会覆盖这个新的 future 的值。

import scala.concurrent.ExecutionContext.Implicits.global

val intFuture = Future {
    10 / 0
}

val eventualInt: Future[Int] = intFuture transform(
    result => {
        println("this is a valid result.")
        10 * result
    }, ex => {
        println("some wrong has happened.")
        ex.getCause
    }
)

eventualInt onComplete {
    case Success(result) =>
    println(s"final result is : $result")
    case Failure(cause) =>
    println(s"see cause : $cause")
}

Thread.sleep(2000)

组合 Future

zip 方法

zip 方法可以将两个 future 的结果整合成一个二元组 (x,y) 的形式并返回。不过,如果任意一个 future 失败了,它将抛出一个异常。如果两个 future 都失败了,则抛出前一个 future 的异常,或者说调用 zip 方法的那一个。

val futureX = Future {
    Thread.sleep(2000)
    200
}

val futureY = Future {
    Thread.sleep(3000)
    300
}

// 将 200 和 300 拉链成一个 (200,300)。
val zipFuture: Future[(Int, Int)] = futureX zip futureY

zipFuture onComplete {
    case Success(res) =>
    println(res)
    case Failure(cause) =>
    println(cause)
}

Thread.sleep(4000)

fold 方法

Future 伴生对象提供了 fold 方法,用来累计多个 future 的计算结果值。同样,如果任意一个 future 失败,则会导致最终的 future 也失败。如果多个 future 执行失败,则 fold 方法只抛出第一个出现的异常。

import scala.concurrent.ExecutionContext.Implicits.global

val futureX = Future {

  println(Thread.currentThread().getName)
  Thread.sleep(1000)
  200
}

val futureY = Future {
  println(Thread.currentThread().getName)
  Thread.sleep(1500)
  300
}

// 将多个 future 装入一个集合当中。每个 future 会交给不同的 worker 线程去处理。
val eventualInts = List(futureX,futureY)

//柯里化函数。 fold 方法将从 0 开始累计,并进行累计操作。
val finalResult: Future[Int] = Future.fold(eventualInts)(0) {
  (int1, int2) => {
    int1 + int2
  }
}

Thread.sleep(2000)

finalResult onComplete {
  case Success(sum) => println(sum)
  case _ => println("failed.")
}

本章涉及的依赖以及用途

下面是本章节中与 Future 有关的相关依赖:

import scala.concurrent.ExecutionContext.Implicits.global
	// => Scala Future 所依赖的执行上下文。
import scala.concurrent.duration._
	// => 用于 Await 的时间表示。

参考资料

  1. [CSDN] Scala : Future 的理解及使用
  2. [IBM] Scala:Scala 中的异步事件处理
  3. [百度文库] 离散数学 最短路径和关键路径
  4. [简书] 解读 Scala 中的 async/await
  5. [CSDN] Scala新手指南中文版 -第九篇 (Promise和Future实践)