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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
auto promise = std::promise<std::string>();

auto producer = std::thread([&]
{
promise.set_value("Hello World");
});

auto future = promise.get_future();

auto consumer = std::thread([&]
{
std::cout << future.get();
});

producer.join();
consumer.join();

从上述代码中可以看出,C++ Promise 包含了 Future,可以通过 get_future 方法获取 Future 对象。两者有明确的分工,Promise 提供了 set_value 方法支持写操作,Future 提供了 get 方法支持读操作。

Scala Future & Promise

在 Scala 中,同样如此,Future 和 Promise 可作为同一个异步操作的两个部分。

  • Future 作为一个可提供只读占位符,用于存储未来值的对象。
  • Promise 作为一个实现一个 Future,并支持可写操作的单一赋值容器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import scala.concurrent.{ Future, Promise }
import scala.concurrent.ExecutionContext.Implicits.global

val p = Promise[T]()
val f = p.future

val producer = Future {
val r = produceSomething()
p success r
continueDoingSomethingUnrelated()
}

val consumer = Future {
startDoingSomething()
f onSuccess {
case r => doSomethingWithResult()
}
}

从上述代码中可以看出,Scala Promise 同样包含了 Future,可以通过 future 属性获取 Future 对象。Promise 提供了 successfailure 等方法来更新状态。Future 提供了 onSuccessonFailure 等方法来监听未来值。

独立实现

其他很多编程语言中,并不同时包含 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 状态更新为 fulfilledrejected 状态。

返回机制

在返回机制方面,Future 以传统的 return 方式返回结果。如下所示为 Dart 中 Future 的返回机制示例,其返回正如普通的方法一样,通过 return 完成。

1
2
3
4
5
Future<String> _readFileAsync() async {
final file = File(filename);
final contents = await file.readAsString();
return contents.trim();
}

而 Promise 通常将结果作为闭包参数进行传递,并执行闭包从而实现返回。如下所示为 JavaScript 中 Promise 的返回机制示例,resolve 是一个只接受成功值的闭包,其参数为 Image 类型;reject 是一个只接受错误值的闭包,其参数为 Error 类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function loadImageAsync(url) {
return new Promise(function(resolve, reject) {
const image = new Image();

image.onload = function() {
resolve(image);
};

image.onerror = function() {
reject(new Error('Could not load image at ' + url));
};

image.src = url;
});
}

语言实现

下面,我们来看一下各种编程语言是如何独立实现 Future 或 Promise 的。

Dart

Dart 内置提供了标准 Future 实现,其同时提供了 asyncawait 关键字分别用于描述异步函数和等待异步函数。如下所示,为 Dart 中的 Future 应用示例。

1
2
3
4
5
6
7
8
9
10
11
Future<String> createOrderMessage() async {
var order = await fetchUserOrder();
return 'Your order is: $order';
}

Future<String> fetchUserOrder() {
return Future.delayed(
const Duration(seconds: 2),
() => 'Large Latte',
);
}

C

C# 提供了 Task,其本质上类似于一种 Future 实现。此外,C# 还提供了异步函数关键字 asyncawait,分别用于描述异步函数和等待异步函数。如下所示,为 C# 中的使用示例。

1
2
3
4
5
6
7
8
9
async Task<int> AccessTheWebAsync() {   
HttpClient client = new HttpClient();
Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");
DoIndependentWork();
string urlContents = await getStringTask;
return urlContents.Length;
}

string urlContents = await client.GetStringAsync();

Swift

Swift 提供了 Task,其本质是一种加强版的 Future 实现。Swift 通过提供额外的 TaskGroup 的概念,使其同时支持结构化并发和非结构化并发。此外,Swift 也提供的 async await 关键字支持异步函数,基于此,Swift 也能够实现和其他语言一样的 Future 实现。如下所示,为 Swift 中类似于 Future 的使用示例。

1
2
3
4
5
let newPhoto = // ... some photo data ...
let handle = Task {
return await add(newPhoto, toGalleryNamed: "Spring Adventures")
}
let result = await handle.value

Java

Java 1.5 提供了 FutureFutureTask,其中 Future 是一个接口,FutureTask 是一种实现,它们提供了一种相对标准的 Future 实现。其通过 RunnableCallable 进行实例化,有一个无参构造器,FutureFutureTask 支持外部只读,FutureTask 的 set 方法是 protected,未来值只能由内部进行设置。如下所示,为基于 FutureTask 的一个应用示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
Task task = new Task();
FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
executor.submit(futureTask);
executor.shutdown();

try {
System.out.println("task result: "+ futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}

Java 8 提供了 CompletableFuture,其本质上是一种 Promise 的实现。按照我们之前的定义,Future 是只读的,Promise 是可写的,而 CompletableFuture 提供了可由外部调用的状态更新方法,因此可以将其归类为 Promise。另一方面,CompletableFuture 又实现了 Future 的读取方法 get。整体上,CompletableFuture 混合了 Future 和 Promise 的能力。如下所示,为 CompletableFuture 的一个应用示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Supplier<Integer> momsPurse = ()-> {
try {
Thread.sleep(1000);//mom is busy
} catch (InterruptedException e) {
;
}
return 100;
};

ExecutorService ex = Executors.newFixedThreadPool(10);

CompletableFuture<Integer> promise =
CompletableFuture.supplyAsync(momsPurse, ex);
promise.thenAccept(u->System.out.println("Thank you mom for $" + u ));
promise.complete(10);

JavaScript

从 ES6 开始,JavaScript 支持了 Promise 的经典实现,同时支持了 asyncawait 关键字用于描述异步任务。使用 async 关键字修饰函数的返回值是一个 Promise 对象。await 关键字修饰一个 Promise 对象,表示等待异步任务的值,有点类似等待 Future。如下所示,为 JavaScript 中 Promise 的使用示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Sleep {
constructor(timeout) {
this.timeout = timeout;
}
then(resolve, reject) {
const startTime = Date.now();
setTimeout(
() => resolve(Date.now() - startTime),
this.timeout
);
}
}

(async () => {
const sleepTime = await new Sleep(1000);
console.log(sleepTime);
})();

总结

本文简单介绍了一下 Future 和 Promise 的发展历史。然后,分别介绍了两者在实现中的关系和区别。同时,介绍了 Future 和 Promise 在各种编程语言中的实现。

后续有时间,我们在来深入研究一下编程语言层面是如何支持 Future 和 Promise 。

参考

  1. Multilisp: A Language for Concurrent Symbolic Computation
  2. The Incremental Garbage Collection of Process
  3. Futures and Promises
  4. Futures and Promises
  5. Future和Promise的区别
  6. What's the difference between a Future and a Promise?
  7. Futures and Promises
  8. Under the hood of Futures and Promises in Swift
  9. Futures vs. Promises
  10. What is std::promise?
  11. std::promise
  12. threads, promises, futures, async, C++
  13. FUTURE和PROMISE
  14. Java并发编程:callable、Future和FutureTask
  15. Class FutureTask
  16. ECMAScript 6 入门
  17. Netty Promise
  18. Netty 中的异步编程 Future 和 Promise
  19. So what's a thunk?