4

Future 和 Promise

 1 year ago
source link: http://chuquan.me/2022/12/05/future-and-promise/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

从异步与并发编程兴起以来,学术界与工业界提出了非常多的解决方案,本文将要介绍的 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:作为异步操作的生产者。
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,并支持可写操作的单一赋值容器。
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 完成。

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

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

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

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

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 的使用示例。

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

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

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 的一个应用示例。

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 的使用示例。

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 。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK