1.理解 Future 和 Promise
现代化的 Future 隐式地处理了两种情况:失败与延迟。要了解如何把阻塞式 IO 转化成非阻塞式 IO,我们必须学习一些不同的表示失败处理和延迟处理的抽象概念。
刚开始可能会显得有点困难,但是一旦真正理解了,大多数开发者就能够习惯这种编程范式了。
1.1.Future——在类型中表达失败与延迟
像 ask 模式这样的异步 API 会返回一个占位符,类似前面提到的 Future 类型。我们可以了解如何使用不同的方法在测试用例中与 PongActor 进行交互,以及如何让代码变得越来越简洁。
1.2.准备 Java 示例
首先,在 Java 8 的例子中,为了避免冗余,我们将 ask 方法简化,并封装到一个方法中。这样看上去就像一个真正的异步 API 了:
public CompletionStage<String> askPong(String message){
Future sFuture = ask(actorRef, "Ping", 1000);
CompletionStage<String> cs = toJava(sFuture);
return cs;
}
接着我们就可以来创建简单的测试用例:
@Test
public void printToConsole() throws Exception {
askPong("Ping").
thenAccept(x -> System.out.println("replied with: " + x));
Thread.sleep(100);
}
1.3.关于休眠
这个测试并没有进行任何断言,但是已经展示了真正的异步行为。我们可以通过观察运行效果来确认异步操作是否成功(在这个测试用例中,我们希望打印到控制台)。
如果希望事件异步发生的话,我们可能时不时地需要将测试线程休眠。和阻塞一样,在测试中休眠线程是没问题的,不过在任何时候都不应该在真正的代码中休眠线程。尽管这些测试并不进行真实的测试,但是它们对于帮助我们观察异步操作实验的效果是很有用的。在花一些时间理解 Future 之后,我们将学习如何在异步代码中进行断言。
2.剖析 Future
Future[T]/CompletableFuture<T>
成功时会返回一个类型为 T 的值,失败时则会返回Throwable
。我们将分别学习如何处理这两种情况(成功与失败),以及如何将 Future 的值转换成有用的结果。
2.1.成功情况的处理
在测试中已经看到,PongActor 会在接收到“Ping”时返回“Pong”。我们将使用这个例子来展示与 Future 进行交互的不同方法。
2.1.1.对返回结果执行代码
有时候我们需要使用返回结果做一些“简单的事情”。可能是将事件记录到日志,也可能是通过网络返回一个响应。我们可以“注册”事件,一旦 Future 的结果返回就执行这个事件。
就像上面的例子中那样,在 Java 8 中,可以使用 thenAccept
来操作返回结果。
askPong("Ping").thenAccept(x -> System.out.println("replied with: " + x));
2.1.2.对返回结果进行转换
最常见的一种用例就是在处理响应之前先异步地对其进行转换。
例如,我们可能需要从数据库获取数据,然后将其转换成一个 HTTP 响应,再返回给客户端。
在 Java 8 中,我们调用
thenApply:askPong("Ping").thenApply(x -> x.charAt(0))
上面的操作会返回一个新的 Future,包含 Char 类型。我们可以在对返回结果进行转换后将新得到的 Future 再传递给其他方法,做进一步处理。
2.1.3.对返回结果进行异步转换
如果需要进行异步调用,那么首先要对返回结果进行另一个异步调用,这样代码就会看上去有一点乱:
//Java
CompletionStage<CompletionStage<String>> futureFuture =
askPong("Ping").thenApply(x -> askPong(x));
很多情况下,需要进行一个异步调用,然后像上面的例子中一样,在得到结果后进行另一个异步调用。不过这样一来,结果就会嵌套在两层 Future 中了。
这种情况是很难处理的,要将结果扁平化,使得结果只在一个 Future 中,我们需要的是一个Future[String]/CompletionStage[String]
。
有很多方法都可以用来做这样的链式异步操作。在 Java 中使用 thenCompose:
CompletionStage<String> cs = askPong("Ping").thenCompose(x ->
askPong("Ping")
);
一旦对第一个“Ping”做出了响应,就发送第二个“Ping”并返回包含结果值的 Future作为响应。
注意到我们可以继续像这样把异步操作连接到一起。这是一种进行流数据处理的很强大的方法。我们可以向一个远程服务发起调用,然后使用得到的结果向另一个服
务发起调用。
其中任何一个调用失败都会导致整个 Future 失败。接下来我们就来看一下失败的情况。
2.2.失败情况的处理
失败情况是有可能发生的,而我们也需要去处理这些失败情况。所有的失败情况最终都会由一个 Throwable 来表示。和成功的情况类似,有许多方法可以帮助我们来处理失败情况,甚至是从失败中恢复。
2.2.1.在失败情况下执行代码
很多时候,我们都想要在失败情况下做些什么。最基本的就是在失败情况下向日志中打印一些信息。
在 Scala 中,有一种很简单的方法支持这种需求:onFailure。这个方法接受一个部分函数作为参数,而这个部分函数接受一个 Throwable。
不幸的是,在 Java 8 中,没有面向用户的用于失败处理的方法,因此我们在这里引入 handle()
来处理这种情况:
askPong("cause error").handle((x, t) -> {
if(t != null){
System.out.println("Error: " + t);
}
return null;
});
handle 接受一个 BiFunction
作为参数,该函数会对成功或失败情况进行转换。handle中的函数在成功情况下会提供结果,在失败情况下则会提供 Throwable,因此需要检查Throwable 是否存在(结果和 Throwable 中只有一个不是 null)。
如果 Throwable 存在,就向日志输出一条语句。由于我们需要在该函数中返回一个值,而失败情况下又不需要对返回值做任何操作,因此直接返回 null。
2.2.2.从失败中恢复
很多时候,在发生错误的时候我们仍然想要使用某个结果值。如果想要从错误中恢复的话,可以对该 Future 进行转换,使之包含一个成功的结果值。
CompletionStage<String> cs = askPong("cause error")
.exceptionally(t -> {
return "default";
});
2.2.3.异步地从失败中恢复
我们经常需要在发生错误时使用另一个异步方法来恢复,下面是两个用例。
- 重试某个失败的操作。
- 没有命中缓存时,需要调用另一个服务的操作。
下面展示了一个重试操作:
askPong("cause error")
.handle( (pong, ex) -> ex == null
? CompletableFuture.completedFuture(pong)
: askPong("Ping")
).thenCompose(x -> x);
我们需要分两步来完成这一操作。首先,检查 exception 是否为 null。如果为 null,就返回包含结果的 Future,否则返回重试的 Future。接着,调用 thenCompose 将CompletionStage[CompletionStage[String]]
扁平化。
2.3.构造 Future
很多时候,我们需要执行多个操作,而且可能想要在代码库的不同位置来执行这些操作。之前介绍到的每个方法调用都会返回一个新的 Future,而我们又可以对这个新的 Future 执行其他操作。
3.链式操作
我们已经介绍了 Future 的基本使用方法。应用函数式风格来处理延迟和失败的好处之一就是可以把多个操作组合起来,而在组合的过程中无需处理异常。我们可以把注意力放在成功的情况上,在链式操作的结尾再收集错误。
之前介绍的每个用于结果转换的方法都会返回一个新的 Future,可以处理这个 Future,也可以将其与更多操作链接到一起。
总结一下,执行多个操作时,我们最后使用一个恢复函数来处理所有可能发生的错误。可以用我们想要的任何顺序来组合这些函数(combinators)来完成我们需要完成的工作。
askPong("Ping")
.thenCompose(x -> askPong("Ping" + x)
.handle((x, t) -> {
if(t != null) {
return "default";
} else {
return x;
}
});
在上面的例子中,我们得到了一个 Future,然后调用 thenCompose/flatMap,在第一个操作完成时异步地发起另一个调用。接着,在发生错误时,我们使用一个 String 值来恢复错误,保证 Future 能够返回成功。
在执行操作链中的任一操作时发生的错误都可以作为链的末端发生的错误来处理。这样就形成了一个很有效的操作管道,无论是哪个操作导致了错误,都可以在最后来处理异常。我们可以集中注意力描述成功的情况,无需在链的中间做额外的错误检查。可以在最后单独处理错误。
4.组合 Future
我们经常需要访问执行的多个 Future。同样有很多方法可以用来处理这些情况。在Java 中,可以使用 CompletableFuture 的 thenCompose 方法,在 Future 的值可用时访问到这些值:
askPong("Ping")
.thenCombine(askPong("Ping"), (a,b) -> {
return a + b; //"PongPong"
});
这个例子展示了一种处理多个不同类型 Future 的机制。通过这种方法,可以并行地执行任务,同时处理多个请求,更快地将响应返回给用户。这种对并行的使用可以帮助我们提高系统的响应速度。
5.处理 Future 列表
如果想要对集合中的每个元素执行异步方法,那么可以使用 Future 列表。
例如 ,在 Scala
中,如果我们有一个消息列表,对于列表中的每个消息,向 PongActor 发送查询,最后会得到如下的一个 Future 列表:
val listOfFutures: List[Future[String]] = List("Pong", "Pong",
"failed").map(x => askPong(x))
对 Future 列表的处理并不容易。我们希望得到的是一个结果列表,也就是要反转一下类型,把 List[Future]转换成 Future[List]。Future 的 sequence 方法就是用来完成这一工作的:
val futureOfList: Future[List[String]] = Future.sequence(listOfFutures)
现在我们就有了一个可以使用的类型。例如,如果在 futureOfList 上调用 map 方法的话,就可以得到一个 List[String],这就是我们想要的结果类型。不过这里有一个问题。一旦 Future 列表中的任何一个 Future 返回失败,那么 sequence 生成的 Future 也会返回失败。如果不希望这种情况发生,想要得到一些成功的结果值,那么可以在执行 sequence之前将返回失败的 Future 逐一恢复:
Future.sequence(listOfFutures.map(future => future.recover {
case Exception => ""
}))
在 Java 8 的核心库中,并没有提供具备类似功能的方法。不过我们能找到一些代码示例用于实现类似的功能。
6.Future 速查表
操作 | ScalaFuture | Java CompletableFuture |
---|---|---|
Transform Value | .map(x => y) | .thenApply(x -> y) |
Transform Value Async | .flatMap(x => futureOfY) | .thenCompose(x -> futureOfY) |
Return Value if Error | .recover(t => y | .exceptionally(t -> y) |
Return Value Async if Error | .recoverWith(t => futureOfY) | .handle(t,x -> futureOfY).thenCompose(x->x) |