概念梳理
基础概念
正如我们将在后续章节中看到的那样,我们选择称其为“概念”,其确切定义往往会有所不同。我们将从对 futures/promises 的概念的尽可能广泛的定义开始,然后放大并涵盖不同语言对这些构造的解释之间的许多语义差异。一个 Future 或者 Promise 可以看做那些暂时不可得但是最终会被得到的值。换句话说,它是对时间概念进行编码的抽象。通过选择使用此构造,可以假定您的值现在可以具有许多可能的状态,具体取决于我们请求它的时间点。最简单的变化包括两个与时间有关的状态:
- completed/determined:计算完毕,可以获得值。
- incomplete/undetermined:计算还在进行中。
稍后我们将看到,在 futures/promises 的某些变体中引入了其他状态,以更好地支持错误处理和取消之类的需求。重要的是,futures/promises 通常可以实现一定程度的并发。也就是说,在 Futures 的第一个定义中:
The construct (future X) immediately returns a future for the value of the expression X and concurrently begins evaluating X.When the evaluation of X yields a value, that value replaces the future.
- (Halstead, 1985)
futures/promises 的某些解释具有与之相关的类型,而另一些则没有。通常,未来/Promise 是单项任务;也就是说,它只能被写入一次。一些解释是阻塞的(同步的),另一些则是完全非阻塞的(异步的)。某些解释必须显式启动(即手动启动),而在其他解释中,计算是隐式启动的。受函数编程的启发,此构造的不同解释之间的主要区别之一与流水线或组合有关。对 futures/promises 的一些较流行的解释使链接操作或定义在完成由 futures/promises 表示的计算时要调用的操作的流水线成为可能。这与大量使用回调或更强制性的直接阻止方法形成对比。
源起与应用
作为相关主题的 futures/promises 的兴起在很大程度上与并行和并发编程以及分布式系统的兴起同时发生。这自然而然地遵循,因为作为对时间进行编码的抽象,当延迟成为问题时,futures/promises 引入了一种很好的方式来推断状态变化;当一个节点必须与分布式系统中的另一个节点进行通信时,程序员通常会面临这些问题。但是,futures/promises 在许多其他情况下也被认为是有用的,无论是分布式的还是无用的。此类上下文包括:
-
Request-Response Patterns,例如通过 HTTP 的网络服务调用。Futures 可以用来表示 HTTP 请求的响应值。
-
Input/Output,例如需要用户输入的 UI 对话框,或例如从磁盘读取大文件的操作。Futures 可以用来表示 IO 调用和 IO 的结果值(例如,终端输入,已读取文件的字节数组)。
-
Long-Running Computations,想象一下,您希望启动长时间运行的计算(例如复杂的数值算法)的过程不要等待该长时间运行的计算完成,而是继续处理其他任务。未来可能会用来表示这种长期运行的计算及其结果的价值。
-
Database Queries,像长时间运行的计算一样,数据库查询可能很耗时。因此,像上面一样,可能希望将执行查询的工作转移到另一个进程,然后继续处理下一个任务。Futures 可以用来表示查询和查询的结果值。
-
RPC (Remote Procedure Call),当对服务器进行 RPC 调用时,网络延迟通常是一个问题。像上面一样,可能希望不必等待 RPC 调用的结果,而只需将其卸载到另一个进程即可。Futures 可以用来表示 RPC 调用及其结果。当服务器响应结果时,Futures 就完成了,其价值就是服务器的响应。
-
Reading Data from a Socket 可能由于网络延迟而非常耗时。因此,可能希望不必等待传入的数据,而是将其卸载到另一个进程。Futures 可以用来表示读取操作以及 Futures 完成时读取的结果值。
-
Timeouts,例如管理 Web 服务中的超时。表示超时的 Future 可能只返回没有结果或某种空结果,例如类型化编程语言中的 Unit 类型。
今天,许多现实世界中的服务和系统都在诸如此类的流行环境中大量使用 futures/promises,这要归功于对未来的看法或对诸如 JavaScript,Node.js,Scala,Java,C++等流行语言和框架的 Promise。正如我们将在其他章节中看到的那样,futures/promises 的这种扩散导致 futures/promises 随着时间和跨语言而改变了含义和名称。
相似概念
Future, Promise, Delay 以及 Deferred 通常指的是大致相同的同步机制,其中对象充当迄今未知结果的代理。当结果可用时,然后执行其他一些代码。多年来,这些术语已指代语言和生态系统之间略有不同的语义。在部分语言中,可能直接存在像 Future, Promise, Delay 以及 Deferred 这样的类或可构造函数。不过像 Scala、Java 以及 Dart 中,它们都是将 Future 与 Promise 当做不同的对象进行处理:
- Future 是对于那些尚待计算的值的只读引用。
- Promise 或 CompletableFuture 或 Completer 是与 Future 关联的单赋值对象。
简单来说,Future 是对于某个写入到 Promise 的值的只读的窗口;我们可以通过调用 Promise 的 future 方法来获取到 Future 对象。这两个概念在 Scala 中的定义如下:
A future is a placeholder object for a result that does not yet exist.A promise is a writable, single-assignment container, which completes a future.Promises can complete the future with a result to indicate success, or with an exception to indicate failure.
Scala Future 与 Java(6)Future 之间的重要区别在于,Scala Future 本质上是异步的。Java 的未来(至少到 Java 6 之前)都处于阻塞状态。Java 7 将异步 Future 引入了人们的极大关注。在 Java 8 中,Future 接口具有一些方法来检查计算是否完成,等待其完成以及在计算完成时检索计算结果。可以将 CompletableFutures
视为一个 Promise,因为可以明确设置它们的值。但是,CompletableFuture
还实现了 Future
接口,使其也可以用作 Future
。使用公共设置方法可以将 Promise 视为未来,调用方(或其他任何人)可以使用该方法来设置未来的价值。
在 JavaScript 世界中,JQuery 引入了 Deferred 对象的概念,这些对象用于表示尚未完成的工作单元。Deferred 对象包含一个 Promise 对象,表示该工作单元的结果。Promise 是函数返回的值。Deferred 的对象也可以由其调用方取消。类似于 Scala 与 Java,C# 也区分了 Promise 与 Future。在 C# 中,Futures 被称为 Task<T>
,Promise 被称为 TaskCompletionSource<T>
。Future 的结果存放在了 Task<T>.Result
,TaskCompletionSource<T>.Task<TResult>
则包含了完成 Task
对象或者抛出异常的方法。
JavaScript 规范中还定义了单独的 Promise 对象,(Promises/A+, 2013)
规范紧急定义了接口而将具体的实现留给了各个标准。JavaScript 中的 Promise 同样是异步的,并且可以流式调用。如我们所见,Future/Promise 的语言和库实现之间的概念,语义和术语似乎有所不同。术语和语义上的这些差异源于悠久的历史和独立的语言社区,这些社区激增了对 Future/Promise 的使用。
简史
如下的时间轴是对于 futures/promises 的简史的梳理:
最终导致 futures/promises 的第一个概念出现在 1961 年,当时有所谓的 thunks。Thunk 可以被看作是对 futures/promises 的原语,顺序的概念。根据其发明者 P.Z.Ingerman 的说法,thunks 是:
A piece of coding which provides an address
(Ingerman, 1961)
更详细的 futures/promises 的简史参考这里。