热门问题
时间线
聊天
视角
Future與promise
异步处理 来自维基百科,自由的百科全书
Remove ads
在計算機科學中,future、promise、delay和deferred,是在某些並發編程語言中,指稱用於同步程序執行的一種構造。由於某些計算尚未結束,故而需要一個對象來代理這個未知的結果。這種構造起源於函數式編程和相關范型如邏輯編程,其目的是將值與其運算過程解耦,從而允許更靈活地進行計算,特別是通過將其並行化來進行。後來它在分布式計算中得到了應用,用來減少網絡通信往返的延遲。再後來async/await語法機制使其變得更有用,籍此允許以直接風格編寫異步程序,而不再採用續體傳遞風格。
術語
在1976年Daniel P. Friedman和David Wise提出了術語「promise」[1],同年Peter Hibbard稱之為「eventual」[2]。1977年Henry Baker和Carl Hewitt在一篇論文中介紹了一個類似的概念「future」[3]。
術語「future」、「promise」、「delay」和「deferred」通常用來稱謂同樣一種機制,在特定實現中可能只選用其中之一。在這種情況下,值與其運算過程是一起創建並且相互關聯的:future若指稱推遲設置的一個值[4],設置它的函數就可稱謂為promise;promise若指稱推遲設置的一個值[5],設置它的函數就可稱謂為resolver;promise若指稱一個異步函數[1],它的結果值所設置的就可稱謂為future。設置一個推遲值也稱為「決定/解決」(resolve)、「履行/實現」(fulfil)或「綁定」(bind)它。
推遲值及其設置者也存在區分稱謂而同時使用的情況,二者的這種用法在不同實現中的差異將在專門章節中討論。具體來說,future指稱一個「只讀」的變量的占位符視圖,而promise指稱一個可寫的單賦值容器,使用其「成功」方法來設置這個future的值[6]。尤其是在定義future之時,無須指定設置其值的promise;並且可能有不同的promise,設置同一個future的值;但是對於一個給定future,僅可以進行一次設置。
Remove ads
歷史
future及/或promise構造,首先實現於編程語言例如MultiLisp[4]和Act 1之中。在並發邏輯編程語言中,使用非常類似於future的邏輯變量進行通信[7]。這種語言開始於1982年的「Prolog with Freeze」和「IC Prolog」,並且在後來的語言中變成了真正的並發原語,比如Concurrent Prolog、Parlog、守衛霍恩子句(GHC)、Strand、Janus、Oz(Mozart)和Alice ML[8][9]。叫做「I-變量」的單賦值變量,非常類似於並發邏輯變量,它起源於數據流程編程語言Id的「I-結構」元件[10],並且包含在Reppy的Concurrent ML中[11]。
在1988年Barbara Liskov和Liuba Shrira,發明了promise流水線技術來克服傳輸延遲[5];Liskov和Shrira在論文中使用了術語「promise」,但是他們採用了現在少見的名稱「call-stream」來提及流水線機制。在大約1989年Mark S. Miller、Dean Tribble和Rob Jellinghaus,於Xanadu項目中也獨立發明了此技術[12]。
Liskov和Shrira的論文中描述的設計,以及Xanadu中的promise流水線的實現,都有一個限制,即promise值不是頭等的:call或send的參數或返回值,不能直接是promise。在Liskov和Shrira論文中使用的編程語言Argus,直至大約1988年停止開發,似乎都未曾在任何公開發布中實現promise和call-stream[13][14]。Xanadu實現的promise流水線,僅在1999年Udanax Gold的源代碼發布時才公開發布[15],並且在任何已發布的文檔中都沒有解釋過[16]。
一些早期的演員語言,包括Act系列[17][18],支持並行消息傳遞和流水線式消息處理,但不支持promise流水線。Joule和E語言的後續實現,支持完全頭等的promise和resolver,還有promise流水線。
2000年之後,由於消息模式的請求-響應模型,在用戶界面響應力和Web開發中的應用,future和promise重新引起了人們的興趣。現在一些主流語言對future和promise有了語言支持,最著名的是2004年發行的Java 5中的FutureTask
[19],以及2012年發行的.NET框架 4.5中的async
/await
結構[20][21],它在很大程度上受到可追溯到2007年的「F#異步編程模型」的啟發[22][23]。async/await隨後被其他語言採用,特別是2014年的Dart 1.9[24]、2014年發行的Hack(HHVM)、2015年的Python 3.5[25]、ECMAScript 2017、2019年的Rust 1.39和C++20等。
Remove ads
編程語言典型示例
Python的concurrent.futures
模塊,提供了異步執行可調用對象的高層接口。異步執行可以使用線程池執行器ThreadPoolExecutor
,通過多個線程來進行;或使用進程池執行器ProcessPoolExecutor
,通過分立的多個進程來進行。concurrent.futures.Future
類封裝了可調用對象的異步執行,其實例由執行器抽象類Executor
的這兩個子類的submit()
方法創建[26]。
在下面的例子中,定義了load_url()
,用來檢索一個URL所指定的單一網頁並報告其內容。使用with
語句指定採用線程池執行器,這確保了線程由它及時清理。用這個執行器的submit()
方法啟動所有的裝載操作任務,並使用字典推導式為其創建的每個future標記上對應的URL。採用模塊函數as_completed()
,在指定的Future
類的諸實例「已齊全」(completed)之時,建立在其上的迭代器。
import concurrent.futures
import urllib.request
URLS = [
'https://www.python.org/',
'https://pypi.org/search/',
'https://no-such-url']
def load_url(url, timeout):
with urllib.request.urlopen(url, timeout=timeout) as conn:
return conn.read()
with concurrent.futures.ThreadPoolExecutor() as executor:
future_to_url = {executor.submit(load_url, url, 60) : url for url in URLS}
for future in concurrent.futures.as_completed(future_to_url):
url = future_to_url[future]
try:
data = future.result()
except Exception as exc:
print(f'{url!r} generated an exception: {exc!s}')
else:
print(f'{url!r} page is {len(data):d} bytes')
在CPython中,由於全局解釋器鎖(GIL),保證一時只有一個線程可以執行Python字節碼;一些擴展模塊被設計為在進行計算密集任務時釋放GIL,還有在進行I/O時總是釋放GIL。線程池執行器的max_workers
參數的缺省值是min(32, os.cpu_count() + 4)
,這個缺省值為I/O踴躍任務保留至少5個worker線程,為釋放GIL的CPU踴躍任務利用最多32個CPU邏輯核心。想要更好利用多核心機器的計算資源,可以使用進程池執行器或多進程模塊multiprocessing
。對於同時運行多個I/O踴躍任務,仍然適合採用線程池執行器或線程模塊threading
。
Python的異步I/O模塊asyncio
,是採用async/await語法編寫並發代碼的庫[27]。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
,則對等待時間沒有限制。
Remove ads
在JavaScript中,Promise
對象表示一個異步運算的最終完成或失敗,及其結果值或失敗理由。Promise
對象是對一個值的代理(proxy),這個值在創建這個promise之時不必需已知。promise允許為異步行動的最終成功值或失敗理由關聯上處理器,這使得異步方法像同步方法那樣返回值:並非立即返回最終值,異步方法返回一個promise來在將來的某一點上提供這個值。
在JavaScript生態系統中,Promise
對象在成為語言本身的一部份之前很久就有了多種實現[28]。儘管各有不同的內部表示,至少幾乎所有類似Promise
的對象都實現了「可接續」(thenable)接口[29]。可接續對象實現了.then()
方法,Promise
對象就是可接續的。
一個promise被稱為已落實(settled),如果它要麼已履行(fulfilled)要麼已拒絕(rejected),而不再待定(pending)。Promise
實例的.then()
方法,接受一到二個實際參數,它們是作為處理器異步執行的回調函數,分別針對了這個promise的已履行和已拒絕情況;此方法返回一個新的promise,它決定(resolve)出其所調用處理器的返回值,或者在原來這個promise未被處理的情況下,仍決定出其已落實的值。Promise
實例的.catch()
方法,實際上就是置空針對已履行情況的回調函數,而只有針對已拒絕情況的回調函數的.then()
方法。
Promise()
構造子創建Promise
對象,它主要用於包裝仍基於回調的API所提供的異步運算,要注意它只能通過new
算子來構造:new Promise(executor)
。Promise()
構造子的實際參數叫做「執行器」(executor),它是由這個構造子同步執行的一個函數,它有可自行指定名字的兩個函數形式參數,比如指定為resolveFunc
和rejectFunc
,這兩個函數接受任何類型的單一實際參數。
Promise()
構造子返回的Promise
對象,在要麼resolveFunc
函數要麼rejectFunc
函數被調用之時,它就成為「已決定」(resolved)的。要注意如果在調用這兩個函數之一的時候,將另一個Promise
對象作為實際參數傳遞給它,則這個Promise
對象可以稱為已決定但仍未落實。在執行器中有任何錯誤拋出,都會導致這個promise成為已拒絕狀態,而返回值會被忽略。
Promise
類提供四個靜態方法來實施異步任務並發,其中的Promise.allSettled()
方法,接受有多個promise的可迭代對象作為輸入,當所有輸入的promise都已落實之時,返回一個單一的已履行的Promise
對象,它決定出描述每個輸入promise的結果的一個對象數組;其中每個對象都有status
屬性,它是字符串"fulfilled"
或者"rejected"
;還有在status
為"fulfilled"
時出現的屬性value
,或者在status
為"rejected"
時出現的屬性reason
。
在下面例子中,全局fetch()
方法,啟動從網絡上取回一個資源的過程,並返回一旦響應可獲得就履行的一個promise;這個promise決定出表示對這個請求響應的一個Response
對象,它只在遇到網絡錯誤之時拒絕。Response.text()
方法返回一個promise,它決定出這個響應的主體的一個文本表示。
const urls = [
'https://developer.mozilla.org/',
'https://javascript.info/promise-api',
'https://no-such-url'
];
const fetchPromises = urls.map((url) => fetch(url));
Promise.allSettled(fetchPromises)
.then((results) => {
results.forEach((result, num) => {
if (result.status == "fulfilled") {
if (result.value.ok) {
result.value.text()
.then((data) => {
console.log(`${urls[num]}: ${data.length} bytes`);
})
.catch((err) => {
console.error(err);
});
}
else {
console.log(`${urls[num]}: ${result.value.status}`);
}
}
else if (result.status == "rejected") {
console.error(`${urls[num]}: ${result.reason}`);
}
});
});
JavaScript是天然的單線程的,所以在給定時刻只有一個任務在執行,但控制可以在不同的promise之間轉移,使得多個promise表現為並發執行。在JavaScript中並行執行只能通過分立的worker後台線程來完成[30]。
同基於promise的代碼合作的一種更簡單的方式,是使用async/await關鍵字。在函數開始處增加async
,使其成為異步函數,異步函數總是返回一個promise。在異步函數中,可以在對返回一個promise的函數的調用之前,使用await
關鍵字;這使得代碼在這一點上等待直到這個promise已落實下來,然後在這一點上promise的已履行的值被當作返回值,而已拒絕的理由被作為例外拋出。可以使用try...catch
塊進行例外處理,就如同這個代碼是同步的一樣。
Remove ads
實現列表
下面列出支持future、promise和並發邏輯變量、數據流程變量或I-變量的語言,包括直接在語言中支持還有在標準庫中支持:
- ABCL/f[31]
- Alice ML,支持future[9][32]
- AmbientTalk,包含頭等的resolver和只讀promise
- C++,從C++11開始有std::future和std::promise
- Concurrent ML,僅限I-變量和M-變量[11]
- Crystal
- Dart,使用Future/Completer類[33]以及關鍵字
async
/await
[24] - Elm,通過Task模塊[34]
- Glasgow Haskell,僅限同步可變變量「MVars」[35]
- Id,僅限I-結構和M-結構[10]
- Io[36]
- Java,通過Future[37]或CompletableFuture[38]
- JavaScript,始於ECMAScript 2015提供
Promise
對象[39],並且自從ECMAScript 2017可通過關鍵字async
/await
來使用[40] - Lucid,僅限作為串流的變量
- 一些Lisp
- .NET,自.NET框架 4.0起通過TPL(任務並行庫)[43]
- C#,自.NET框架 4.5起[20],通過關鍵字
async
/await
[21] - Visual Basic,自從版本11,通過關鍵字
Async
/Await
[21]
- C#,自.NET框架 4.5起[20],通過關鍵字
- Kotlin,但是
kotlin.native.concurrent.Future
通常只在書寫本機運行代碼時使用[44] - Nim,使用
async
/await
語法 - Oxygene
- Oz版本3 [45]
- Python,自從2011年版本3.2提供concurrent.futures模塊[26][46],這是由PEP 3148所提議[47],而2015年版本3.5增加了基於低層future的
asyncio
模塊,它採用高層async
/await
語法[48] - R,延遲計算的promise,仍然是單線程
- Raku[49]
- Rust,通常經由
.await
完成[50] - Scala,通過scala.concurrent包[51]
- Scheme,僅延遲求值
- Squeak Smalltalk
- Strand
還支持promise流水線的語言包括:
- E
- Joule
Remove ads
- 對於Common Lisp:
- 對於C++:
- 對於C#和其他.NET語言:
- Parallel Extensions庫
- 對於Groovy:
- GPars[62]
- 對於JavaScript:
- Promises/A+標準網站[29],有此標準的實現的列表[63]
- Cujo.js[64]的when.js[65],提供的promise符合Promises/A+ 1.1標準
- Dojo Toolkit,提供promise和Twisted風格Deferred[66]
- MochiKit[67],受Twisted的Deferred的啟發
- jQuery[68]的Deferred對象[69],基於了CommonJS Promises/A規定[70]
- AngularJS[71]
- Q[72],符合Promises/A+ 1.1標準
- RSVP.js[73],符合Promises/A+ 1.1標準
- Bluebird[74]
- Closure庫的promise包[75],符合Promises/A+標準
- 對於Java:
- 對於Objective-C:
- 對於OCaml:
- Lazy模塊實現了懶惰的顯式future[85]
- 對於Perl:
- 對於PHP:
- React/Promise [89]
- 對於Python:
- 對於R:
- 對於Ruby:
- 對於Rust:
- futures-rs[97]
- 對於Scala:
- Twitter的util庫[98]
- 對於Swift:
- 對於Tcl:
- tcl-promise[105]
Remove ads
future可以用協程或生成器實現[25],從而具有相同的求值策略(例如協同多任務或延遲求值)。
future還可以很容易地用通道實現:future是一個單元素的通道,而promise是一個發送到通道,實現future的過程。這允許future在支持通道(如CSP和Go)的並發編程語言中實現[106]。由此產生的future是顯式的,因為它們必須通過從通道讀取而不是僅僅通過求值來獲取。
只讀視圖
在某些編程語言(如Oz、E和AmbientTalk)中,可以獲得推遲值的「只讀視圖」,允許在解決(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對象」表示只讀視圖。[107]
- 在Alice ML中,future提供「只讀視圖」[32],而promise包含future和解決future的能力[108]。
- 在.NET 4.0中,
System.Threading.Tasks.Task<T>
表示只讀視圖。解決值可以通過System.Threading.Tasks.TaskCompletionSource<T>
來完成。
對只讀視圖的支持符合最小特權原則,因為它使得設置值的能力僅限於需要設置該值的主體。在同樣支持流水線的系統中,異步消息(具有結果)的發送方接收結果的只讀promise,消息的目標接收resolver。
Remove ads
有關結構
「future」是事件同步原語的特例,它只能完成一次。通常,事件可以重置為初始的空狀態,因此可以根據需要多次完成。[109]
「I-var」是具有下面定義的阻塞語義的future。它起源於Id語言中包含I-var的「I-structure」數據結構。可以使用不同值多次設置的有關同步構造稱為「M-var」。M-var支持take
(採取)或put
(放置)當前值的原子性操作,這裡採取這個值還將M-var設置回其初始的「空」狀態。[110]
「並發邏輯變量」與future類似,但是通過合一更新,與邏輯編程中的「邏輯變量」相同。因此,它可以多次綁定到可合一的值,但不能設置回到空或未解決狀態。Oz的數據流變量充當並發邏輯變量,並且還具有上面提到的阻塞語義。
「並發約束變量」是並發邏輯變量的一般化,以支持約束邏輯編程:約束可以多次「縮小」,表示可能值的較小集合。通常,有一種方法可以指定每當約束進一步縮小時應該運行的thunk;這是支持「約束傳播」所必需的。
Remove ads
隱式與顯式future
對future的使用可以是「隱式」的,任何對future的使用都會自動獲得它的值,它就像是普通的引用一樣;也可以是「顯式」的,用戶必須調用函數來獲取值,例如Java中的Future[37]或CompletableFuture[38]的get
方法。獲得一個顯式的future的值可以稱為「刺激」(stinging)或「強迫」(forcing)。顯式future可以作為庫來實現,而隱式future則通常作為語言的一部分來實現。
最初的Baker和Hewitt論文描述了隱式future,它們在演員模型和純面向對象編程語言(如Smalltalk)中自然得到支持。Friedman和Wise的論文只描述了顯式的future,可能反映了在老舊硬件上有效實施隱式future的困難。難點在於老舊硬件不能處理原始數據類型(如整數)的future。例如,add指令不會處理3 + future factorial(100000)
。在純演員模型或面向對象語言中,這個問題可以通過向future factorial(100000)
發送消息+[3]
來解決,它要求future自己加3
並返回結果。請注意,無論factorial(100000)
何時完成計算,消息傳遞方法都可以工作,而且不需要任何「刺激」或「強迫」。
阻塞與非阻塞語義
如果future的值是異步訪問的,例如通過向它發送消息,或者通過使用類似於E語言中的when
的構造顯式地等待它,那麼在消息可以被接收或等待完成之前,推遲直到future得到解決(resolve)是沒有任何困難的。這是在純異步系統(如純演員語言)中唯一需要考慮的情況。
然而,在某些系統中,還可能嘗試「立即」或「同步」訪問future的值。這樣的話就需要做出一個設計選擇:
- 訪問可能會阻塞當前線程或進程,直到future得到解決(可以具有超時)。這是Oz語言中「數據流變量」的語義。
- 嘗試的同步訪問總是會引發信號指示錯誤,例如拋出異常。這是E語言中遠程promise的語義。[111]
- 潛在的,如果future已經解決,則訪問可能成功,但如果未解決,則發出信號指示錯誤。這樣做的缺點是引入了不確定性和潛在的競爭條件,這似乎是一種不常見的設計選擇。
作為第一種可能性的示例,在C++11中 ,需要future值的線程可以通過調用wait()
或get()
成員函數來阻塞,直到它可獲得為止。還可以使用wait_for()
或wait_until()
成員函數指定等待超時,以避免無限期阻塞。如果future對std::async
的調用,那麼阻塞等待(沒有超時)可能導致函數的同步調用以計算等待線程上的結果。
promise流水線
在分布式系統中使用推遲值可以顯著地減少傳輸延遲。例如,指稱推遲值的promise,成就了「promise流水線」[112][113],就像在E語言和Joule語言中實現的那樣,它在Argus語言中稱為「call-stream」[5]。
考慮一個涉及常規遠程過程調用的表達式,例如:
t3 := (x.a()).c(y.b())
可以展開為
t1 := x.a(); t2 := y.b(); t3 := t1.c(t2);
每個語句需要發送一條消息,並在下一個語句可以繼續之前收到一個答覆。例如,假設x
、y
、t1
和t2
都位於同一台遠程機器上。在這種情況下,在開始執行第三條語句之前,必須對該機器進行兩次完整的網絡往返。然後,第三條語句將引起另一個到同一個遠程機器的往返。
上面的表達式可以使用E語言的語法寫為:
t3 := (x <- a()) <- c(y <- b())
其中x <- a()
表示將消息a()
異步發送給x
。它可以展開為:
t1 := x <- a(); t2 := y <- b(); t3 := t1 <- c(t2);
所有三個變量都會立即為其結果分配promise,執行過程將繼續進行到後面的語句。之後嘗試解決t3
的值可能會導致傳輸延遲;但是,流水線操作可以減少所需的往返次數。如果與前面的示例一樣,x
、y
、t1
和t2
都位於相同的遠程機器上,則流水線實現可以用一次往返來計算t3
,不必用三次。由於所有三條消息都指向同一遠程計算機上的對象,因此只需要發送一個請求,只需要接收一個包含結果的響應。另請注意,即使t1
和t2
位於不同機器上,或者位於與x
或y
不同的機器上,發送t1 <- c(t2)
也不會阻塞。
promise流水線應與並行異步消息傳遞區分開來。在支持並行消息傳遞但不支持流水線操作的系統中,上面示例中的消息發送x <- a()
和y <- b()
可以並行進行,但發送t1 <- c(t2)
將不得不等到t1
和t2
都被接收,即使x
、y
、t1
和t2
在同一個遠程機器上。在涉及許多消息的更複雜情況下,流水線的相對傳輸延遲優勢變得更大。
promise流水線操作也不應與演員系統中的流水線式消息處理相混淆,在這種系統中,演員可以在完成當前消息的處理之前,指定並開始執行下一個消息的行為。
有特定線程的future
某些語言比如Alice ML,定義的future可以關聯着計算這個future值的特定線程[32]。這種計算可以通過spawn exp
,在創建future時及早地開始;或者通過lazy exp
,在首次需要其值時懶惰地開始。在延遲計算的意義上,懶惰的future類似於thunk 。
Alice ML還支持可由任何線程解決的future,並稱謂它們為「promised future」[108]。這裡的promise是給future的顯式把柄(handle),它通過多態庫函數Promise.promise
來創建,所有promise都關聯的一個future,創建一個新promise也就創建了一個新鮮的future;這個future通過Promise.future
來提取,並且通過顯式的應用Promise.fulfill
於對應的promise來消除,即在全局中將這個future替代為一個值。Alice ML不支持promise流水線,轉而對於future,包括關聯着promise的future,流水線是自然而然地發生的。
在沒有特定線程的future(如Alice ML所提供的)中,通過在創建這個future的同時創建一個計算這個值的線程,可以直接實現及早求值的有特定線程的future。在這種情況下,最好將只讀視圖返回給客戶,以便僅讓新創建的線程能夠解決這個future。
要在沒有特定線程的future中,實現隱式惰性的有特定線程的future,需要一種機制來確定何時首次需要future的值(例如,Oz中的WaitNeeded
構造[114] )。
如果所有值都是對象,那麼有實現透明轉發對象的能力就足夠了,因為發送給轉發器的首條消息表明需要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 <Expression>
的表達式,以它對具有環境E和客戶C的Eval
消息的響應方式來定義:future表達式通過向客戶C發送新創建的演員F(計算<Expression>
的響應的代理)作為返回值來響應Eval
消息,與之並發的向<Expression>
發送具有環境E和客戶F的Eval
消息。F的默認行為如下:
- 當F收到請求R時,它會檢查是否已經收到來自求值
<Expression>
的響應(可以是返回值或拋出異常),處理過程如下所示:- 如果它已經有了響應V,那麼
- 如果V是返回值,那麼向它發送請求R。
- 如果V是一個異常,那麼就把這個異常拋給請求R的客戶。
- 如果它還沒有響應,那麼將R存儲在F內的請求隊列中。
- 如果它已經有了響應V,那麼
- 當F接收到來自求值
<Expression>
的響應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
是否大於其自身的請求。
參見
引用
外部連結
Wikiwand - on
Seamless Wikipedia browsing. On steroids.
Remove ads