Future 和 Promise
从异步与并发编程兴起以来,学术界与工业界提出了非常多的解决方案,本文将要介绍的 Future 和 Promise 正是其中的两种解决方案。Future 和 Promise 的实现理念非常相似,两者在发展过程中相互借鉴,相互融合。目前,很多流行的语言和框架都引入了 Future 和 Promise 的概念,如:JavaScript、Node.js、Scala、Java、C++ 等。
本文,我们来简单聊一聊 Future 和 Promise 历史和设计,以及两者之间的关系与区别。
历史简介
1962. Thunk
关于 Future 和 Promise 的起源,最早可以追溯到 1961 年的 Thunk。根据创造者 P.Z. Ingerman 的描述,Thunk 是提供地址的一段代码。
Thunk 被设计为一种将实际参数绑定到 Algol-60 过程调用中的正式定义的方法。如果用表达式代替形式参数调用过程,编译器会生成一个 thunk,它将执行表达式并将结果的地址留在某个标准位置。
目前,thunk 的用法仍然非常广泛,我在 《Swift 泛型协议》 一文中也提到过 thunk 的释义。
1977. Future
1977 年,Henry C. Baker 和 Hewitt 在论文《The Incremental Garbage Collection of Process》中首次提到 Future。
他们提出了一个新的术语 call-by-future
,用于描述一种基于
Future 的调用形式。当将表达式提供给执行器时,将返回该表达式的
.future
。如果表达式返回类型为值类型,那么当未来表达式计算得到值时,会将值返回。这里会为每一个
future
都会创建一个进程,并立即执行表达式。如果表达式已完成,则值立即可用;如果表达式未完成,则请求进程等待表达式执行完成。
在论文中,Future 主要由三部分组成:
- 进程(Process):用于执行表达式的进程。
- 单元(Cell):可写入值的内存地址,用于存储表达式的未来值。
- 队列(Queue):等待未来值的进程列表。
从 Future 的概念我们可以看出,论文所提到的 Future 几乎已经和现代的 Future 概念非常接近了。
1985. Multilisp
1985 年,Robert H. Halstead 在论文《Multilisp: A Language for
Concurrent Symbolic Computation》中提出的 Multilisp 语言支持了基于
future
注解的 call-by-future
能力。
在 Multilisp 中,如果变量绑定到 Future 的表达式,则会自动创建一个新的进程。表达式会在新的进程中执行,一旦执行完成,则将计算结果保存至变量引用中。通过这种方式,Multilisp 支持在新进程中同时计算任意表达式的能力。因此,也支持无需等待 Future 完成,继续执行其他计算的能力。这样的话,如果 Future 的值从未使用过,那么整个进程就不会被阻塞,从而消除了潜在的死锁源。
相比于 1977 年提出的 Future,Mutilisp 实现的 Future 支持在特定情况下不阻塞进程,从而一定程度上优化了程序的执行效率。
1988. Promise
1988 年,Liskov 和 Shrira 在论文《Distributed Programming in Argus》中提出的 Argus 语言设计了一种称为 Promises 的结构。
与 Multilisp 中的 Future 类似,Argus 中的 Promise 也提供一个用于存储未来值的占位符。Promise 的特别之处在于,当调用 Promise 时,会立即创建并返回一个 Promise,并在新进程中进行类型安全的异步 PRC 调用。当异步 PRC 调用执行完毕,由调用者设置返回值。
设计理念
经过数十年的发展,Future 和 Promise 的设计理念整体上非常相似,但是在不同的语言和框架实现中又存在一定的区别,对此,这里我们基于最广泛的定义进行介绍。
整体实现
在 Scala、C++ 等编程语言中,同时包含两种结构分别对应 Future 和 Promise。作为整体实现,Future 和 Promise 可被视为同一异步编程技术中的两个部分:
- Future:表示异步任务的 返回值,表示一个未来值的占位符,即 值的消费者。
- Promise:表示异步任务的 执行过程,表示一个值的生产过程,即 值的生产者。
在同时包含 Future 和 Promise 的实现中,一般 Promise 对象会有一个关联的 Future 对象。当 Promise 创建时,Future 对象会自动实例化。当异步任务执行完毕,Promise 在内部设置结果,从而将值绑定至 Future 的占位符中。Future 则提供读取方法
将异步操作分成 Future 和 Promise 两个部分的主要原因是 为了实现读写分离,对外部调用者只读,对内部实现者只写。
下面,我们以几种语言中的实现来分别进行介绍。
C++ Future & Promise
在 C++ 中,Future 和 Promise 是一个异步操作的两个部分。
std::future
:作为异步操作的消费者。std::promise
:作为异步操作的生产者。
1 | auto promise = std::promise<std::string>(); |
从上述代码中可以看出,C++ Promise 包含了 Future,可以通过
get_future
方法获取 Future 对象。两者有明确的分工,Promise
提供了 set_value
方法支持写操作,Future 提供了
get
方法支持读操作。
Scala Future & Promise
在 Scala 中,同样如此,Future 和 Promise 可作为同一个异步操作的两个部分。
Future
作为一个可提供只读占位符,用于存储未来值的对象。Promise
作为一个实现一个 Future,并支持可写操作的单一赋值容器。
1 | import scala.concurrent.{ Future, Promise } |
从上述代码中可以看出,Scala Promise 同样包含了 Future,可以通过
future
属性获取 Future 对象。Promise 提供了
success
、failure
等方法来更新状态。Future
提供了 onSuccess
、onFailure
等方法来监听未来值。
独立实现
其他很多编程语言中,并不同时包含 Future 和 Promise 两种结构,比如:Dart 只包含 Future,JavaScript 只包含 Promise,甚至有些编程语言混淆了 Future 和 Promise 的原始区别。
在独立实现中,Future 和 Promise 各自都有着相对比较统一的表示形式,在实现方面的差异也相对比较一致,主要包括以下几个方面区别:
- 状态表示
- 状态更新
- 返回机制
状态表示
在状态表示方面,Future 只有两种状态:
uncomplete
:表示未完成状态,即未来值还未计算出来。completed
:表示已完成状态,即未来值已经计算出来。当然计算结果可以分为值或错误两种情况。
对于 Promise,一般使用三种状态进行表示:
pending
:待定状态,即 Promise 的初始状态。fulfilled
:满足状态,表示任务执行成功。rejected
:拒绝状态,表示任务执行失败。
无论是 Future 还是 Promise,状态转移的过程都是不可逆的。
状态更新
在状态更新方面,Future 的状态由
内部进行自动管理。当异步任务执行完成或抛出错误时,其状态将隐式地自动从
uncomplete
状态更新为 completed
状态。
对于 Promise,其状态由
外部进行手动管理。通常由开发者根据控制流逻辑,执行特定的状态更新方法显式地从
pending
状态更新为 fulfilled
或
rejected
状态。
返回机制
在返回机制方面,Future 以传统的 return
方式返回结果。如下所示为 Dart 中 Future
的返回机制示例,其返回正如普通的方法一样,通过 return
完成。
1 | Future<String> _readFileAsync() async { |
而 Promise
通常将结果作为闭包参数进行传递,并执行闭包从而实现返回。如下所示为
JavaScript 中 Promise 的返回机制示例,resolve
是一个只接受成功值的闭包,其参数为 Image
类型;reject
是一个只接受错误值的闭包,其参数为
Error
类型。
1 | function loadImageAsync(url) { |
语言实现
下面,我们来看一下各种编程语言是如何独立实现 Future 或 Promise 的。
Dart
Dart 内置提供了标准 Future
实现,其同时提供了
async
和 await
关键字分别用于描述异步函数和等待异步函数。如下所示,为 Dart 中的 Future
应用示例。
1 | Future<String> createOrderMessage() async { |
C
C# 提供了 Task
,其本质上类似于一种 Future 实现。此外,C#
还提供了异步函数关键字 async
和
await
,分别用于描述异步函数和等待异步函数。如下所示,为 C#
中的使用示例。
1 | async Task<int> AccessTheWebAsync() { |
Swift
Swift 提供了 Task
,其本质是一种加强版的 Future
实现。Swift 通过提供额外的 TaskGroup
的概念,使其同时支持结构化并发和非结构化并发。此外,Swift 也提供的
async
await
关键字支持异步函数,基于此,Swift
也能够实现和其他语言一样的 Future 实现。如下所示,为 Swift 中类似于
Future 的使用示例。
1 | let newPhoto = // ... some photo data ... |
Java
Java 1.5 提供了 Future
和 FutureTask
,其中
Future
是一个接口,FutureTask
是一种实现,它们提供了一种相对标准的 Future 实现。其通过
Runnable
和 Callable
进行实例化,有一个无参构造器,Future
和
FutureTask
支持外部只读,FutureTask
的 set
方法是
protected
,未来值只能由内部进行设置。如下所示,为基于
FutureTask
的一个应用示例。
1 | public class Test { |
Java 8 提供了 CompletableFuture
,其本质上是一种 Promise
的实现。按照我们之前的定义,Future 是只读的,Promise 是可写的,而
CompletableFuture
提供了可由外部调用的状态更新方法,因此可以将其归类为
Promise。另一方面,CompletableFuture
又实现了 Future
的读取方法 get
。整体上,CompletableFuture
混合了 Future 和 Promise 的能力。如下所示,为
CompletableFuture
的一个应用示例。
1 | Supplier<Integer> momsPurse = ()-> { |
JavaScript
从 ES6 开始,JavaScript 支持了 Promise 的经典实现,同时支持了
async
和 await
关键字用于描述异步任务。使用
async
关键字修饰函数的返回值是一个 Promise
对象。await
关键字修饰一个 Promise
对象,表示等待异步任务的值,有点类似等待 Future。如下所示,为 JavaScript
中 Promise
的使用示例。
1 | class Sleep { |
总结
本文简单介绍了一下 Future 和 Promise 的发展历史。然后,分别介绍了两者在实现中的关系和区别。同时,介绍了 Future 和 Promise 在各种编程语言中的实现。
后续有时间,我们在来深入研究一下编程语言层面是如何支持 Future 和 Promise 。
参考
- Multilisp: A Language for Concurrent Symbolic Computation
- The Incremental Garbage Collection of Process
- Futures and Promises
- Futures and Promises
- Future和Promise的区别
- What's the difference between a Future and a Promise?
- Futures and Promises
- Under the hood of Futures and Promises in Swift
- Futures vs. Promises
- What is std::promise?
- std::promise
- threads, promises, futures, async, C++
- FUTURE和PROMISE
- Java并发编程:callable、Future和FutureTask
- Class
FutureTask
- ECMAScript 6 入门
- Netty Promise
- Netty 中的异步编程 Future 和 Promise
- So what's a thunk?