在计算机科学中,future、promise、delay和deferred,是在某些并发编程语言中,指称用于同步的一种构造。由于某些计算尚未结束,故而需要一个对象来代理这个未知的结果。这种构造起源于函数式编程和相关范型如逻辑编程,其目的是将值与其运算过程解耦,从而允许更灵活地进行计算,特别是通过将其并行化来进行。后来它在分布式计算中得到了应用,用来减少网络通信往返的延迟。再后来async/await语法机制使其变得更有用,籍此允许以直接风格编写异步程序,而不再采用续体传递风格。
术语
在1976年Daniel P. Friedman和David Wise提出了术语“promise”,同年Peter Hibbard称之为“eventual”。1977年和在一篇论文中介绍了一个类似的概念“future”。
术语“future”、“promise”、“delay”和“deferred”通常用来称谓同样一种机制,在特定实现中可能只选用其中之一。在这种情况下,值与其运算过程是一起创建并且相互关联的:future若指称推迟设置的一个值。尤其是在定义future之时,无须指定设置其值的promise;并且可能有不同的promise,设置同一个future的值;但是对于一个给定future,仅可以进行一次设置。
历史
future及/或promise构造,首先实现于编程语言例如和Act 1之中。在语言中,使用非常类似于future的逻辑变量进行通信。这种语言开始于1982年的“Prolog with Freeze”和“IC Prolog”,并且在后来的语言中变成了真正的并发原语,比如Concurrent Prolog、、守卫霍恩子句(GHC)、、、Oz(Mozart)和,并且包含在Reppy的Concurrent ML中。
在1988年Barbara Liskov和Liuba Shrira,发明了promise流水线技术来克服传输延迟;Liskov和Shrira在论文中使用了术语“promise”,但是他们采用了现在少见的名称“call-stream”来提及流水线机制。在大约1989年、Dean Tribble和Rob Jellinghaus,于Xanadu项目中也独立发明了此技术。
Liskov和Shrira的论文中描述的设计,以及Xanadu中的promise流水线的实现,都有一个限制,即promise值不是头等的:call或send的参数或返回值,不能直接是promise。在Liskov和Shrira论文中使用的编程语言Argus,直至大约1988年停止开发,似乎都未曾在任何公开发布中实现promise和call-stream。Xanadu实现的promise流水线,仅在1999年Udanax Gold的源代码发布时才公开发布,并且在任何已发布的文档中都没有解释过。
一些早期的演员语言,包括Act系列,支持并行消息传递和流水线式消息处理,但不支持promise流水线。和E语言的后续实现,支持完全头等的promise和resolver,还有promise流水线。
2000年之后,由于的请求-响应模型,在用户界面和Web开发中的应用,future和promise重新引起了人们的兴趣。现在一些主流语言对future和promise有了语言支持,最著名的是2004年发行的Java 5中的FutureTask,以及2012年发行的.NET框架 4.5中的async/await结构,它在很大程度上受到可追溯到2007年的“F#异步编程模型”的启发。async/await随后被其他语言采用,特别是2014年的Dart 1.9、ECMAScript 2017、2019年的Rust 1.39和C++20等。
编程语言典型示例
Python
Python的concurrent.futures模块,提供了可调用对象的高层接口。异步执行可以使用线程池执行器ThreadPoolExecutor,通过多个线程来进行;或使用进程执行器ProcessPoolExecutor,通过分立的多个进程来进行。concurrent.futures.Future类封装了可调用对象的异步执行,其实例由执行器抽象类Executor的这两个子类的submit()方法创建。asyncio模块基于低层事件循环,提供的高层API包括了:并发运行Python协程并拥有在其执行上的完全控制,进行网络IO和IPC,控制子进程,通过队列分布和同步并发代码。asyncio模块的同步原语,在设计上类似于threading模块的同步原语,但与之相比有两个重要差异:asyncio模块的同步原语不是线程安全的,故而不能用于OS线程同步;这些同步原语的方法不接受实际参数。
asyncio模块提供的低层API中有asyncio.Future对象,用来桥接低层基于回调的代码和高层采用async/await语法的代码,它在设计上模仿了concurrent.futures.Future对象。这两个模块的Future对象的set_result()方法标记这个Future对象为“已完毕”(done)并设置它的结果,而set_exception()方法标记这个Future对象为已完毕并设置一个例外;done()方法在这个Future对象已完毕时返回True;result()方法返回这个Future对象所设置的结果,或者引发其所设置的例外。
二者的关键差异包括了:asyncio.Future是可期待(awaitable)对象,而concurrent.futures.Future对象不可以被期待(awaited)。asyncio.Future.result()不接受超时实际参数,如果此刻这个Future对象的结果仍不可获得,它引发一个InvalidStateError例外;而concurrent.futures.Future.result(),可接受(timeout)实际参数,如果此刻结果仍未“完全”(completed),它等待指定时间后若仍未完全则引发TimeoutError例外,如果超时未指定或指定为None,则对等待时间没有限制。
JavaScript
在JavaScript中,Promise对象表示一个运算的最终完成或失败,及其结果值或失败理由。Promise对象是对一个值的代理(proxy),这个值在创建这个promise之时不必需已知。promise允许为异步行动的最终成功值或失败理由关联上处理器,这使得异步方法像同步方法那样返回值:并非立即返回最终值,异步方法返回一个promise来在将来的某一点上提供这个值。
在JavaScript生态系统中,Promise对象在成为语言本身的一部份之前很久就有了多种实现。尽管各有不同的内部表示,至少几乎所有类似Promise的对象都实现了“可接续”(thenable)接口。
同基于promise的代码合作的一种更简单的方式,是使用async/await关键字。在函数开始处增加async,使其成为异步函数,函数总是返回一个promise。在异步函数中,可以在对返回一个promise的函数的调用之前,使用await关键字;这使得代码在这一点上等待直到这个promise已落实下来,然后在这一点上promise的已履行的值被当作返回值,而已拒绝的理由被作为例外抛出。可以使用try...catch块进行例外处理,就如同这个代码是同步的一样。
实现列表
编程语言支持
下面列出支持future、promise和并发逻辑变量、数据流程变量或I-变量的语言,包括直接在语言中支持还有在标准库中支持:
- ABCL/f
- ,支持future以及关键字async/await
- Elm,通过Task模块
*Glasgow Haskell,仅限同步可变变量“MVars”
- ,仅限I-结构和M-结构
- Java,通过Future或CompletableFuture
- JavaScript,始于ECMAScript 2015提供Promise对象,并且自从ECMAScript 2017可通过关键字async/await来使用
- Lucid,仅限作为串流的变量
*一些Lisp
** Clojure
**
- .NET,自.NET框架 4.0起通过TPL()
** C#,自.NET框架 4.5起
- Nim,使用async/await语法
*
- Oz版本3
- Python,自从2011年版本3.2提供concurrent.futures模块,这是由PEP 3148所提议,而2015年版本3.5增加了基于低层future的asyncio模块,它采用高层async/await语法
- R,延迟计算的promise,仍然是单线程
- Raku
- Rust,通常经由.await完成
- Scala,通过scala.concurrent包
- Scheme,仅延迟求值
- Squeak Smalltalk
*
还支持promise流水线的语言包括:
- E
*
非标准库的实现
*对于Common Lisp:
** Blackbird
** Eager Future2
** lparallel
** PCall
*对于C++:
** Boost library
**
** Qt
** Seastar
** Folly
** ,ActiveResult
*对于C#和其他.NET语言:
**库
*对于Groovy:
**GPars
*对于JavaScript:
**Promises/A+标准网站,有此标准的实现的列表
**Cujo.js的when.js,提供的promise符合Promises/A+ 1.1标准
**Dojo Toolkit,提供promise和Twisted风格Deferred
** ,受Twisted的Deferred的启发
** jQuery的Deferred对象,基于了CommonJS Promises/A规定
** AngularJS
**Q,符合Promises/A+ 1.1标准
**RSVP.js,符合Promises/A+ 1.1标准
**Bluebird
**的promise包,符合Promises/A+标准
*对于Java:
**JDeferred,提供了与JQuery.Deferred对象类似的deferred-promise API和行为
**ParSeq,提供task-promise API,由LinkedIn维护,适用于异步流水线和分支
*对于Objective-C:
**MAFuture、RXPromise、ObjC-CollapsingFutures、PromiseKit、objc-promise、OAPromise
*对于OCaml:
**Lazy模块实现了懒惰的显式future
*对于Perl:
**Future、Promises、Reflex
*对于PHP:
**React/Promise
*对于Python:
** promise
** Twisted的Deferred
*对于R:
**future,实现可扩展的future API与懒惰和及早同步和(多核或分布式)异步future
**libuv gem,实现promise
** Celluloid gem,实现future
** future-resource
*对于Rust:
** futures-rs
*对于Scala:
**Twitter的util库
*对于Swift:
**异步框架,实现C#风格的async/非阻塞await
**FutureKit,Apple的GCD(Grand Central Dispatch)实现版本
**FutureLib,纯Swift 2库实现的Scala风格的future和promise与TPL风格的取消
**Deferred,纯Swift库,受到OCaml的Deferred启发
**BrightFutures
*对于Tcl:
**tcl-promise
自行实现
future可以用协程或生成器实现。由此产生的future是显式的,因为它们必须通过从通道读取而不是仅仅通过求值来获取。
只读视图
在某些编程语言(如Oz、E和)中,可以获得推迟值的“只读视图”,允许在解决(resolve)出这个值之后通过它来读取,但不允许通过它来解决这个值:
*在Oz语言中,!!运算符用于获得只读视图。
*在E语言和AmbientTalk中,推迟值由一对称为“promise/resolver对”的值表示。promise表示只读视图,需要resolver来设置推迟值。
*在C++11中,std::future提供了一个只读视图。该值通过使用std::promise直接设置,或使用std::packaged_task或std::async设置为函数调用的结果。
*在Dojo Toolkit的1.5版本的Deferred API中,“仅限consumer的promise对象”表示只读视图。
*在中,future提供“只读视图”,而promise包含future和解决future的能力。
*在.NET 4.0中,System.Threading.Tasks.Task表示只读视图。解决值可以通过System.Threading.Tasks.TaskCompletionSource来完成。
对只读视图的支持符合最小特权原则,因为它使得设置值的能力仅限于需要设置该值的主体。在同样支持流水线的系统中,异步消息(具有结果)的发送方接收结果的只读promise,消息的目标接收resolver。
有关结构
“future”是事件同步原语的特例,它只能完成一次。通常,事件可以重置为初始的空状态,因此可以根据需要多次完成。
“I-var”是具有下面定义的阻塞语义的future。它起源于Id语言中包含I-var的“I-structure”数据结构。可以使用不同值多次设置的有关同步构造称为“M-var”。M-var支持take(采取)或put(放置)当前值的原子性操作,这里采取这个值还将M-var设置回其初始的“空”状态。
“并发逻辑变量”与future类似,但是通过合一更新,与逻辑编程中的“逻辑变量”相同。因此,它可以多次绑定到可合一的值,但不能设置回到空或未解决状态。Oz的数据流变量充当并发逻辑变量,并且还具有上面提到的阻塞语义。
“并发约束变量”是并发逻辑变量的一般化,以支持约束逻辑编程:约束可以多次“缩小”,表示可能值的较小集合。通常,有一种方法可以指定每当约束进一步缩小时应该运行的;这是支持“约束传播”所必需的。
隐式与显式future
对future的使用可以是“隐式”的,任何对future的使用都会自动获得它的值,它就像是普通的引用一样;也可以是“显式”的,用户必须调用函数来获取值,例如Java中的Future
*潜在的,如果future已经解决,则访问可能成功,但如果未解决,则发出信号指示错误。这样做的缺点是引入了不确定性和潜在的竞争条件,这似乎是一种不常见的设计选择。
作为第一种可能性的示例,在C++11中 ,需要future值的线程可以通过调用wait()或get()成员函数来阻塞,直到它可获得为止。还可以使用wait_for()或wait_until()成员函数指定等待超时,以避免无限期阻塞。如果future对std::async的调用,那么阻塞等待(没有超时)可能导致函数的同步调用以计算等待线程上的结果。
promise流水线
在分布式系统中使用推迟值可以显著地减少传输延迟。例如,指称推迟值的promise,成就了“promise流水线”,就像在E语言和语言中实现的那样,它在Argus语言中称为“call-stream” )。
如果所有值都是对象,那么有实现透明转发对象的能力就足够了,因为发送给转发器的首条消息表明需要future的值。
假定系统支持消息传递,在有特定线程的future中,通过让解决线程向future自己的线程发送消息,可以实现没有特定线程的future。然而这可能被视为不必要的复杂性。在基于线程的编程语言中,最具表现力的方法似乎是提供一种混合:没有特定线程的future、只读视图、以及要么有WaitNeeded构造要么支持透明转发。
传future调用
就求值策略而言,“传future调用”是非确定性的:future的值将在创建future和使用其值之间的某个时间进行求值,但确切的时间不确定的,一次运行和另一次运行的求值时间会不一样。计算可以在创建future时开始(及早求值),或者仅在实际需要值时开始(懒惰求值),并且可以在中途暂停,或在一次运行中执行。一旦future被赋值,它就不会在访问future的时候重新计算;这就像传需求调用时使用的记忆化。
“懒惰”future是确定性的具有惰性求值语义的future:future值的计算在首次需要时开始,与传需要调用一样。懒惰future使用在求值策略默认不是懒惰求值的语言中。例如,在C++11中,可以通过将std::launch::deferred启动策略传递给std::async以及计算值的函数来创建这种惰性future。
演员模型中的future语义
在演员模型中,形式为future 的表达式,以它对具有环境E和客户C的Eval消息的响应方式来定义:future表达式通过向客户C发送新创建的演员F(计算的响应的代理)作为返回值来响应Eval消息,与之并发的向发送具有环境E和客户F的Eval消息。F的默认行为如下:
*当F收到请求R时,它会检查是否已经收到来自求值的响应(可以是返回值或抛出异常),处理过程如下所示:
*#如果它已经有了响应V,那么
#如果V是返回值,那么向它发送请求R。
#如果V是一个异常,那么就把这个异常抛给请求R的客户。
*#如果它还没有响应,那么将R存储在F内的请求队列中。
*当F接收到来自求值的响应V时,那么将V存储在F中,并且
**如果V是返回值,那么将所有排队的请求发送到V。
**如果V是一个异常,那么就会把这个异常抛出给每个排队请求的客户。
但是,一些future可以通过特殊方式处理请求以提供更大的并行性。例如,表达式1 + future factorial(n)可以创建一个新的future,其行为类似于数字1+factorial(n) 。这个技巧并不总是有效。例如,以下条件表达式:
: if m>future factorial(n) then print("bigger") else print("smaller")
会挂起,直到factorial(n)这个future已回应询问m是否大于其自身的请求。
参见
*监视器 (程序同步化)
*诅咒金字塔 (编程)
引用
外部链接
*[http://shairosenfeld.com/concurrency.html Concurrency patterns presentation] given at [http://scaleconf.org scaleconf]
- [http://c2.com/cgi/wiki?FutureValue Future Value] and [http://c2.com/cgi/wiki?PromisePipelining Promise Pipelining] at the Portland Pattern Repository
- [http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/84317 Easy Threading with Futures] in Python
评论 (0)