在本文的前面部分中,我们讨论了期货和承诺 。 在最后一部分中,我们将使用其强大的组合器来编写期货。
组成期货:
在第一篇文章中 ,我们看到了如何使用onComplete
, foreach
和使用Await.result
测试用例从Future
提取值。 从单个Future
提取一个值是很好的,但是很多时候我们会产生多个异步操作,并等待多个Future来获得最终结果。 更好的是,有时一个Future
的结果将被馈送到另一个期货或一连串的期货中。
Future
是单子 。 (对不起,我将M-炸弹放到这里,稍后我将解释我对Monoid
, Functor
, Monad
和Applicative
理解) 。 但是现在,让我们忍受这个粗略的解释:
-
Future
是具有某种类型的值的容器(即,它接受类型作为参数,没有它就不能存在)。 您可以拥有Future[Int]
或Future[String]
或Future[AwesomeClass]
–您不能仅拥有简单的Future
。 一个合适的术语是type-constructor 。 相比之下,List
是类型构造函数(也包括Monad)。 列表是值类型为Int
,String
或任何其他类型的值的容器。 没有包含类型的列表/期货不存在。 -
Future
具有flatMap
和unit
函数(因此也具有map
函数)。
之所以提出这一点,是因为我们可以对Option
或List
,而不是使用onComplete
回调或foreach
,而只需map
或flatMap
Future的结果即可。
现在,让我们看一下map
和flatMap
组合器。
映射顺序执行的期货
让我们考虑一下这个简单的任务,它添加了三个数字,这些数字在一定间隔后异步计算。
警告:以下代码杂乱无章,并按顺序执行期货
码
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提供了一种很好的方式来处理具有map
和flatMap
(Monads)的类,以提高理解能力。 对于理解而言,只是语法糖,它被分解为flatMap
和map
。
下面的代码与上面的代码完全相同,除了丑化是由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
,它抛出一个LegacyException
。 recover
函数接受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
块。 此外,如果PartialFunction
内recover
功能不处理最初抛出的异常,异常被传播到调用者。
测试用例
测试用例断言,计算结果是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调用不可用。
类似recover
的recoverWith
接受PartialFunction
。 然而, PartialFunction
一个映射Throwable
到Future
同类型与原始的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是oneFuture
和futureCallingLegacyCodeWithRecoverWith
的结果之和。
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
,而不是一个PartialFunction
像recoverWith
。
测试用例
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
类- async
和await
。
异步的
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
}
如我们所见,以这种方式编写的代码不仅是异步的,而且看起来很自然(实际上,它看起来是同步的)。 我们可以说,理解是使用map
和flatMap
的巨大飞跃,但是async/await
向前迈了一大步。
码
翻译自: https://www.javacodegeeks.com/2016/06/scala-notes-futures-3-combinators-async.html