Scala笔记–期货– 3(组合器和异步)

在本文的前面部分中,我们讨论了期货承诺 。 在最后一部分中,我们将使用其强大的组合器来编写期货。

组成期货:

在第一篇文章中 ,我们看到了如何使用onCompleteforeach和使用Await.result测试用例从Future提取值。 从单个Future提取一个值是很好的,但是很多时候我们会产生多个异步操作,并等待多个Future来获得最终结果。 更好的是,有时一个Future的结果将被馈送到另一个期货或一连串的期货中。

Future单子(对不起,我将M-炸弹放到这里,稍后我将解释我对MonoidFunctorMonadApplicative理解) 。 但是现在,让我们忍受这个粗略的解释:

  1. Future是具有某种类型的值的容器(即,它接受类型作为参数,没有它就不能存在)。 您可以拥有Future[Int]Future[String]Future[AwesomeClass] –您不能仅拥有简单的Future 。 一个合适的术语是type-constructor 。 相比之下, List是类型构造函数(也包括Monad)。 列表是值类型为IntString或任何其他类型的值的容器。 没有包含类型的列表/期货不存在。
  2. Future具有flatMapunit函数(因此也具有map函数)。

之所以提出这一点,是因为我们可以对OptionList ,而不是使用onComplete回调或foreach ,而只需mapflatMap Future的结果即可。

现在,让我们看一下mapflatMap组合器。

映射顺序执行的期货

让我们考虑一下这个简单的任务,它添加了三个数字,这些数字在一定间隔后异步计算。

警告:以下代码杂乱无章,并按顺序执行期货

class FutureCombinators {

  def sumOfThreeNumbersSequentialMap(): Future[Int] = {
    Future {
      Thread.sleep(1000)
      1
    }.flatMap { oneValue =>
      Future {
        Thread.sleep(2000)
        2
      }.flatMap { twoValue =>
        Future {
          Thread.sleep(3000)
          3
        }.map { thirdValue =>
          oneValue + twoValue + thirdValue
        }
      }
    }
  }
...
...

第一个Future在1秒后返回1,第二个Future在2秒后返回2,第三个Future在3秒后返回3。 嵌套块最终计算出三个值的总和,并返回一个单独的Future[Int]

测试用例

为了计算计算值所花费的时间,我们有一个称为timed的小实用程序函数(在ConcurrentUtils特性内),用于计算并打印块花费的时间。

我们Await.result上做的结果阻塞等待futureCombinators.sumOfThreeNumbersSequentialMap 。 我们还对总执行时间进行计时并打印。

class FutureCombinatorsTest extends FunSpec with Matchers with ConcurrentUtils {

  describe("Futures") {
    it("could be composed using map") {
      val futureCombinators = new FutureCombinators
      val result = timed(Await.result(futureCombinators.sumOfThreeNumbersSequentialMap(), 7 seconds))
      result shouldBe 6
    }
  }
...
...
}

trait ConcurrentUtils {  
  def timed[T](block: => T): T = {
    val start = System.currentTimeMillis()
    val result = block
    val duration = System.currentTimeMillis() - start
    println(s"Time taken : $duration")
    result
  }
}

输出量

Time taken : 6049

该函数花了6秒钟多一点的时间执行,表明期货是按顺序执行的。

使用理解语法糖代替Map

Scala提供了一种很好的方式来处理具有mapflatMap (Monads)的类,以提高理解能力。 对于理解而言,只是语法糖,它被分解为flatMapmap

下面的代码与上面的代码完全相同,除了丑化是由Scala编译器完成的。

def sumOfThreeNumbersSequentialForComprehension(): Future[Int] = {
    for {
      localOne <- Future {
        Thread.sleep(1000)
        1
      }
      localTwo <- Future {
        Thread.sleep(2000)
        2
      }
      localThree <- Future {
        Thread.sleep(3000)
        3
      }
    } yield localOne + localTwo + localThree
  }
测试用例

和上面一样

it("could be composed using for comprehensions") {
      val futureCombinators = new FutureCombinators
      val result = timed(Await.result(futureCombinators.sumOfThreeNumbersSequentialForComprehension(), 7 seconds))
      result shouldBe 6
    }

输出量

Time taken : 6012

并行执行期货

如我们所见,前面的代码块顺序运行三个Future,因此总共需要6秒钟才能完成计算。 那不好 我们的期货需要并行运行。 为了实现这一点,我们要做的就是提取Future块并分别声明它们。

val oneFuture: Future[Int] = Future {
    Thread.sleep(1000)
    1
  }

  val twoFuture: Future[Int] = Future {
    Thread.sleep(2000)
    2
  }

  val threeFuture: Future[Int] = Future {
    Thread.sleep(3000)
    3
  }

现在,让我们使用理解来计算值。

def sumOfThreeNumbersParallelMapForComprehension(): Future[Int] = for {  
    oneValue <- oneFuture
    twoValue <- twoFuture
    threeValue <- threeFuture
} yield oneValue + twoValue + threeValue
测试用例

让我们计时一下,并使用以下测试用例声明正确的值。

describe("Futures that are executed in parallel") {
    it("could be composed using for comprehensions") {
      val futureCombinators = new FutureCombinators
      val result = timed(Await.result(futureCombinators.sumOfThreeNumbersParallel(), 4 seconds))
      result shouldBe 6
    }
  }

输出量

Time taken : 3005

如我们所见, sumOfThreeNumbersParallel与最长的Future( threeFuture )花费的时间几乎相同,后者为3秒。

只是为了比较,上面的代码可以不使用for-comprehension编写:

def sumOfThreeNumbersParallelMap(): Future[Int] = oneFuture.flatMap { oneValue =>  
    twoFuture.flatMap { twoValue =>
      threeFuture.map { threeValue =>
        oneValue + twoValue + threeValue
      }
    }
}

警惕的人

就像我们在对List和其他集合(也称为其他Monadic类型)的理解中添加受保护的if子句一样,我们也可以添加针对Future生成器的保护。 下面的if guard检查twoFuture返回的值是否大于1。

def sumOfThreeNumbersParallelWithGuard(): Future[Int] = for {
    oneValue <- oneFuture
    twoValue <- twoFuture if twoValue > 1
    threeValue <- threeFuture
  } yield oneValue + twoValue + threeValue

这些警卫像withFilter一样被withFilter (我90%确信没有人愿意这样写):

def sumOfThreeNumbersMapAndFlatMapWithFilter(): Future[Int] = oneFuture.flatMap { oneValue =>  
    twoFuture.withFilter(_ > 1).flatMap { twoValue =>
      threeFuture.map { threeValue =>
        oneValue + twoValue + threeValue
      }
    }
  }

警惕的人–失败案例

如果防护评估为false,从而生成器产生故障,则将引发NoSuchElementException 。 让我们将防护条件更改为false。

def sumOfThreeNumbersParallelWithGuardAndFailure(): Future[Int] = for {
    oneValue <- oneFuture
    twoValue <- twoFuture if twoValue > 2
    threeValue <- threeFuture
  } yield oneValue + twoValue + threeValue

输出量

Future.filter predicate is not satisfied  
java.util.NoSuchElementException: Future.filter predicate is not satisfied  
    at scala.concurrent.Future$$anonfun$filter$1.apply(Future.scala:280)
    at scala.util.Success$$anonfun$map$1.apply(Try.scala:237)
    at scala.util.Try$.apply(Try.scala:192)
    at scala.util.Success.map(Try.scala:237)
    at scala.concurrent.Future$$anonfun$map$1.apply(Future.scala:237)
    at scala.concurrent.Future$$anonfun$map$1.apply(Future.scala:237)
    at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:32)
    at scala.concurrent.impl.ExecutionContextImpl$AdaptedForkJoinTask.exec(ExecutionContextImpl.scala:121)
    at scala.concurrent.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
    at scala.concurrent.forkjoin.ForkJoinPool$WorkQueue.pollAndExecAll(ForkJoinPool.java:1253)
    at scala.concurrent.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1346)
    at scala.concurrent.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
    at scala.concurrent.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)

异常处理

就像警卫抛出的NoSuchElementException一样,在Future内部异步执行的代码可能会引发各种异常。 尽管有人可能会认为异常不是很像FP,但是很可能在使用分布式应用程序或在Future内部使用Java库时,确实会发生异常。

下面的两个函数都引发异常-第一个函数引发NoSuchElementException ,第二个函数引发LegacyException

//NoSuchElementException
  def throwsNoSuchElementIfGuardFails(): Future[Int] = for {
    oneValue <- oneFuture
    twoValue <- twoFuture if twoValue > 2
    threeValue <- threeFuture
  } yield oneValue + twoValue + threeValue

  //LegacyException

  val futureCallingLegacyCode: Future[Int] = Future {
    Thread.sleep(1000)
    throw new LegacyException("Danger! Danger!")
  }

  def throwsExceptionFromComputation(): Future[Int] = for {
    oneValue <- oneFuture
    futureThrowingException <- futureCallingLegacyCode
  } yield oneValue + futureThrowingException

case class LegacyException(msg: String) extends Exception(msg)
测试用例
describe("Futures that throw exception") {
    it("could blow up on the caller code when guard fails") {
      val futureCombinators = new FutureCombinators
      intercept[NoSuchElementException] {
        val result = timed(Await.result(futureCombinators.throwsNoSuchElementIfGuardFails(), 4 seconds))
      }
    }

    it("could blow up on the caller code when exception comes from a computation executed inside the Future") {
      val futureCombinators = new FutureCombinators
      intercept[LegacyException] {
        val result = timed(Await.result(futureCombinators.throwsExceptionFromComputation(), 4 seconds))
      }
    }
...
...

请注意,即使期货之一导致异常,组合计算的整个结果也将导致异常的传播。

从异常中恢复:

使用recover

如果将来抛出一个scala.util.control.NonFatal ,我们希望有一个默认的备用值,而不是传播错误给调用者,我们可以使用recover功能。 recover非常类似于catch块。

让我们修改上面的函数throwsExceptionFromComputation ,它抛出一个LegacyExceptionrecover函数接受PartialFunction ,该函数从Throwable映射到Future包装的类型。

在下面的代码中,如果futureCallingLegacyCode引发Exception (它确实这样做),则此计算结果的值将设置为200 。 如果未引发异常,则结果值将是该计算本身的结果。

val futureCallingLegacyCodeWithRecover: Future[Int] = futureCallingLegacyCode.recover {
    case LegacyException(msg) => 200
  }

  def recoversFromExceptionUsingRecover(): Future[Int] = for {
    oneValue <- oneFuture
    futureThrowingException <- futureCallingLegacyCodeWithRecover
  } yield oneValue + futureThrowingException

重申一下,如果原始的Future产生成功的值,则将永远不会执行recover块。 此外,如果PartialFunctionrecover功能不处理最初抛出的异常,异常被传播到调用者。

测试用例

测试用例断言,计算结果是oneFuture (为1)和futureCallingLegacyCodeWithRecover (为200)返回的值的总和。

it("could be recovered with a recovery value") {
      val futureCombinators = new FutureCombinators
      val result = timed(Await.result(futureCombinators.recoversFromExceptionUsingRecover(), 2 seconds))
      result shouldBe 201
    }

输出量

Time taken : 1004

使用

在某些情况下,我们可能希望使用其他某些Future结果进行恢复,而不是使用Future导致Exception的值进行恢复。 例如,可以通过对在Server2上运行的另一个服务的HTTP调用来恢复由于网络故障导致对Server1的HTTP调用不可用。

类似recoverrecoverWith接受PartialFunction 。 然而, PartialFunction一个映射ThrowableFuture同类型与原始的Future

就像recover ,如果主Future在其上recoverWith叫失败,那么Future是在映射到PartialFunction被调用。 如果第二个Future产生成功的值,则返回新结果。

val futureCallingLegacyCodeWithRecoverWith: Future[Int] = futureCallingLegacyCode.recoverWith {
    case LegacyException(msg) =>
      println("Exception occurred. Recovering with a Future that wraps 1000")
      Thread.sleep(2000)
      Future(1000)
  }

  def recoversFromExceptionUsingRecoverWith(): Future[Int] = for {
    oneValue <- oneFuture
    futureThrowingException <- futureCallingLegacyCodeWithRecoverWith
  } yield oneValue + futureThrowingException
测试用例

oneFuture花费1秒,恢复的Future花费2秒。 因此,我们将Await.result超时设置为4秒。 最终结果1001是oneFuturefutureCallingLegacyCodeWithRecoverWith的结果之和。

it("could be recovered with a recovery Future") {
      val futureCombinators = new FutureCombinators
      val result = timed(Await.result(futureCombinators.recoversFromExceptionUsingRecoverWith(), 4 seconds))
      result shouldBe 1001
    }

输出量

Time taken : 3006

请注意,就像recover ,如果第二个future也失败,则第二个future引发的错误将传播到调用方。

在下面的代码中,我们创建了另一个Future ,该Exception引发了一条Exception消息Dieded !!! 我们使用此错误抛出功能恢复了第一个Future。 测试用例将揭示第二个未来(恢复一个)的异常被抛出给调用者。

val anotherErrorThrowingFuture: Future[Int] = Future {
    Thread.sleep(1000)
    throw new LegacyException("Dieded!!")
  }

  val futureRecoveringWithAnotherErrorThrowingFuture: Future[Int] = futureCallingLegacyCode.recoverWith {
    case LegacyException(msg) =>
      anotherErrorThrowingFuture
  }

  def recoversFromExceptionUsingRecoverWithThatFails(): Future[Int] = for {
    oneValue <- oneFuture
    futureThrowingException <- futureRecoveringWithAnotherErrorThrowingFuture
  } yield oneValue + futureThrowingException
测试用例
it("when recovered with another Future that throws Exception would throw the error from the second Future") {
      val futureCombinators = new FutureCombinators
      val exception = intercept[LegacyException] {
        timed(Await.result(futureCombinators.recoversFromExceptionUsingRecoverWithThatFails(), 4 seconds))
      }
      exception.msg shouldBe "Dieded!!"
    }

使用fallbackTo:

fallbackTo就像成功值一样,与recoverWith一样。 如果成功,则使用第一个Future的值,或者回退到第二个Future的值。 但是,如果第一个和第二个Future都失败了,那么传播给调用者的错误就是第一个 Future的错误,而不是第二个Future的错误。

让我们使用与recoverWith相同的recoverWith

val futureFallingBackToAnotherErrorThrowingFuture: Future[Int] = futureCallingLegacyCode.fallbackTo (anotherErrorThrowingFuture)

  def recoversFromExceptionUsingFallbackTo(): Future[Int] = for {
    oneValue <- oneFuture
    futureThrowingException <- futureFallingBackToAnotherErrorThrowingFuture
  } yield oneValue + futureThrowingException

请注意, fallbackTo功能只是被动地接受另一个Future ,而不是一个PartialFunctionrecoverWith

测试用例
it("when fallen back to another Future that throws Exception would throw the error from the first Future") {
      val futureCombinators = new FutureCombinators
      val exception = intercept[LegacyException] {
        timed(Await.result(futureCombinators.recoversFromExceptionUsingFallbackTo(), 4 seconds))
      }
      exception.msg shouldBe "Danger! Danger!"
    }

其他有趣且有用的组合器

以下是我认为非常有用的其他Future组合器的简要列表。

压缩

zip工作方式与List.zip 。 它只是合并两个期货,并产生一个Future一的Tuple

def zipTwoFutures:Future[(Int,Int)]=oneFuture zip twoFuture

firstCompletedOf

啊! 当您有两个等效的服务并且一旦最快的服务返回一个值就想继续时, firstCompletedOf会派上用场。

val listOfFutures=List(oneFuture,twoFuture,threeFuture)
  def getFirstResult():Future[Int]=Future.firstCompletedOf(listOfFutures)

在上述情况下, oneFuture返回最快。

序列

sequence是纯魔术。 假设您有一个List[Future[Int]]就像List(oneFuture,twoFuture,threeFuture)并且您要求所有值都以List[Int]返回给您,而不是包装在Future的每个Int 。 该sequence将您的List[Future[Int]]转换为Future[List[Int]]

def getResultsAsList():Future[List[Int]]=Future.sequence(listOfFutures)

我上次使用的是批处理,在该批处理中,我对数据块并行执行逻辑并将其与sequence组合在一起。

Scala异步库

Scala异步库是一个外部项目,可以通过将依赖项添加到build.sbt中来将其添加到项目中

"org.scala-lang.modules" %% "scala-async" % "0.9.6-RC2"

异步库只有两个在其强大的功能scala.async.Async类- asyncawait

异步的

async函数与Future.apply函数非常相似。 实际上,它们的签名几乎相同,我们可以在任何可用的地方用async轻松地替换Future.apply

未来申请

def apply[T](body: =>T)(implicit executor: ExecutionContext): Future[T]

异步

def async[T](body: => T)(implicit execContext: ExecutionContext): Future[T]

除了一般的易读性Future.apply ,在Future.apply上使用async的最主要优点是,对于每个Future生成器(与a理解一起使用时),编译器会产生一个单独的匿名类,而使用async时,它只是一个匿名类类。

因此,我们可以将oneFuture重新编写为

val oneFuture: Future[Int] = async {  
    Thread.sleep(1000)
    1
}

等待

await函数接受Future并返回结果。 但是,与接受Future并返回结果的Await.result是否也不相同? 不。 关键区别在于Await.result是阻塞的,强烈建议不要在测试代码中使用Await.result 。 另一方面, await函数是使用Scala宏实现的,其实现是使用onComplete回调返回Future的结果。

由于async函数返回Future ,因此所有其他错误处理和恢复机制均与以前相同。

让我们用async / await重写前面的三个数之和:

def sumOfThreeNumbersParallelWithAsyncAwait(): Future[Int] = async {
    await(oneFuture) + await(twoFuture) + await(threeFuture)
  }
测试用例
it("could be composed using async/await") {
      val futureCombinators = new FutureCombinators
      val result = timed(Await.result(futureCombinators.sumOfThreeNumbersParallelWithAsyncAwait(), 4 seconds))
      result shouldBe 6
    }

如我们所见,以这种方式编写的代码不仅是异步的,而且看起来很自然(实际上,它看起来是同步的)。 我们可以说,理解是使用mapflatMap的巨大飞跃,但是async/await向前迈了一大步。

代码及其对应的测试用例在github上。

翻译自: https://www.javacodegeeks.com/2016/06/scala-notes-futures-3-combinators-async.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值