Future 和 Promise 是 Vert.X 4.0中的重要角色,貫穿了整個 Vert.X 框架。掌握 Future/Promise 的用法,是用好 Vert.X、編寫高質量非同步程式碼的基礎。本文從 Future/Promise 的概念出發,介紹這兩者的定義以及如何理解其定義;然後介紹 Promise 和 Future 相關的 API,結合若干例項介紹如何編寫非同步程式碼。
1. 概念
Future 和 Promise 是一個寬泛的概念,很多程式語言都有對這二者的實現。在 Java 中,JDK 有對 Future 的實現,不同的 Java 框架(如:Netty)也有各自的實現。
Vert.X 也實現了 Future 和 Promise,並且有自己的定義:Future 表示某種已經發生或未發生的行為的結果,Promise 表示某種已經發生或未發生的行為的寫入端。
A future represents the result of an action that may, or may not, have occurred yet.
A promise represents the writable side of an action that may, or may not, have occurred yet.
這段來自於 Vert.X 原始碼中的描述比較抽象,網路上對這二者有多種不同的描述。自認為“把 Promise 當做訊息的生產者,把 Future 當做訊息的消費者”這一描述最好理解。例如下面一段程式碼,promise 通過 complete() 方法來生產一條訊息,future 通過 onSuccess() 來消費一條訊息。
Promise<String> promise = Promise.promise();
Future<String> future = promise.future();
promise.complete("Hello"); // 生產訊息
future.onSuccess(msg -> System.out.println(msg)); // 消費訊息
從 Java 語法上看,Promise 和 Future 是兩個介面,它們有共同的實現類,在執行時 Fromise 和與之關聯的 Future 是同一個物件。一個 Promise 物件整個生命週期只能夠生產一條訊息,意味著與之關聯的 Future 只能夠消費一條訊息,但可以多次消費這條訊息。
2. Promise
Promise 是一個泛型介面,泛型表示要寫入(生產)的訊息的型別。
2.1 獲取 Promise 例項
獲取 Promise 例項是編寫 Future/Promise 非同步程式碼的第一步,使用者可以通過 Promise 介面提供的靜態工廠方法promise()
來獲取一個 Promise 例項。如下程式碼獲取了一個例項,它可以寫入一條 String 訊息。
Promise<String> promise = Promise.promise();
2.2 Promise 寫入(生產)非同步訊息
Promise 作為行為的寫入端,可以通過 complete 方法來寫入一條訊息。
promise.complete("Hello");
當然,在發生異常時,也可以通過 fail 方法寫入一個異常。如下程式碼,從 path 中讀取一個字串,若讀取成功,則 promise 寫入讀取到的內容;若讀取失敗,則寫入捕獲到的異常。
try {
String str = Files.readString(path);
promise.complete(str); // 寫入一條訊息
} catch(IOException e) {
promise.fail(e); // 寫入一個異常
}
一個 promise 物件只能寫入一次訊息,要麼寫入一條正常訊息,要麼寫入一個異常。通常情況下,往一個已經寫入過內容的 promise 的物件中繼續寫入,會丟擲 IllegalStateException
。除非使用以 "try" 開頭的 API 來寫入訊息,這類 API 通過返回 boolean 值來判斷寫入是否成功,不丟擲 IllegaStateException
。
在 Promise 內部,寫入訊息實質上是把訊息或者異常設定到 Promise 實現類的物件中,並通知與 Promise 關聯的 Future 來處理訊息或異常。
寫入(生產)訊息的 API 清單如下:
方法簽名 | 說明 |
---|---|
void complete(T result) | 寫入非同步任務的結果。 |
void complete() | 同 complete(null) 。 |
void fail(Throwable cause) | 寫入一個異常。 |
void fail(String message) | 同 fail(new NoStackTraceThrowable(message)) 。 |
boolean tryComplete(T result) | 嘗試寫入一個非同步任務的結果。 |
boolean tryComplete() | 同 tryComplete(null) 。 |
boolean tryFail(Throwable cause) | 嘗試寫入一個異常。 |
boolean tryFail(String message) | 同 tryFail(new NoStackTraceThrowable(message)) 。 |
3. Future
Vert.X 官方定義 Future 為已發生或還未發生行為的結果。從另一角度看,相對於生產端的 Promise,Future 又是訊息的消費端。Future 是一個比 Promise 更加複雜的抽象,出場率也更高;很多時候會隱去 Promise,只使用 Future 來完成非同步操作。
3.1 Future 的狀態與結果
Future 有兩個狀態,已完成(已完成 completed)或未完成兩種狀態。而按照 Promise 寫入的結果型別,又可以將已完成狀態分為成功完成和失敗完成。Future 提供了 3 個 API 來查詢狀態。
方法 | 說明 |
---|---|
isComplete() | true 表示 Future 已完成,false 表示未完成。 |
succeed() | true 表示 Future 已成功完成。此時可以讀取正常結果。 |
failed() | true 表示 Future 已經失敗。此時可以讀取異常結果。 |
當 Promise 寫入結果時,對應的 Future 狀態會發生改變,狀態圖如下所示。
當狀態是成功完成(SUCCEED)時,可以通過 future.result() 來讀取一個正常訊息;當狀態是失敗完成(FAILED)時,可以通過 future.cause() 來讀取異常。
方法 | 說明 |
---|---|
result() | 讀取正常結果。若非同步操作未完成或非同步操作失敗,則返回 null;否則,返回非同步操作的正常結果。 |
cause() | 讀取非同步資訊。若非同步操作未完成或非同步操作成功,則返回 null;否則,返回非同步操作的異常結果。 |
以上查詢狀態和讀取結果的方法來自於 Future 的父介面 AsyncResult,這是 Future 能夠表示非同步結果的原因。
3.2 SucceedFuture 與 FailedFuture
與剛建立時狀態不確定的普通 Future 不同,SucceedFuture 和 FailedFuture 在建立出來的時候狀態就已經確定。Future 提供了靜態工廠方法來獲取它們的例項,這一類 Future 物件沒有與之關聯的 Promise,因為訊息已經存在,不需要 Promise 來生產訊息。
Future<Void> future = Future.succeedFuture();
Future<String> future1 = Future.succeedFuture("hello");
Future<Void> future2 = Future.failedFuture(e);
3.3 Future 處理(消費)非同步訊息
前面提到,非同步訊息可能是一條正常訊息,也可能是一個異常;當然,Future 本身也表示非同步訊息(結果)。
Future 提供了 onSuccess, onFailure, 和 onComplete 這幾個 API 來處理非同步訊息。它們的引數都是一個處理器 Handler,也可以看做一個無返回值的函式,這些 API 內部將傳入的處理器包裝稱為監聽器,監聽 Promise 寫入事件。它們返回的結果都是 this,因此可以鏈式呼叫這些方法。
如下程式碼以非同步的形式通過 HttpClient 向遠端伺服器發起請求,獲取響應體。如果執行正常,promise 將響應體作為一條正常訊息寫入;如果發生異常,則寫入一個異常。後面與之關聯的 future 設定了 3 個處理器,當請求成功時,執行 onSuccess 設定的處理器;當請求失敗時,執行 onFailure() 設定的處理器;無論請求成功還是失敗,都將執行 onComplete() 設定的處理器。
Promise<String> promise = Promise.promise();
new Thread(() -> {
try {
String responseBody = httpClient.request("http://www.test.com/user");
promise.complete(responseBody); // 寫入正常訊息
} catch(Exception e) {
promise.fail(e); // 寫入異常
}
}).start();
Future<String> future = promise.future();
future.onSuccess(body -> System.out.println("獲取到 Response Body:" + body)) // 成功時執行
.onFailure(cause -> cause.printStacktrace()) // 失敗時執行
.onComplete(asyncResult -> { // 無論成功失敗都執行
if (asyncResult.succeed()) {
logger.info("成功獲取到 Reponse Body");
} else {
logger.error("獲取 Response Body 失敗。", asyncResult.cause());
}
});
與只能寫入一次結果的 Promise 不同,Future 可以設定任意多個處理器,或者說可以多次消費訊息。
future.onSuccess(result -> // 處理 1)
.onSuccess(result -> // 處理 2);
另外,無論當前 Future 的狀態是成功、失敗或是未完成,都可以呼叫這些 API 來設定處理器(Handler)。如果 Future 是未完成狀態,這些處理器會在 Promise 寫入結果之後觸發執行;如果 Future 是已完成狀態,在設定處理器時立即執行它們。
如下是這幾個 API 的說明。
方法 | 說明 |
---|---|
Future |
處理一個非同步結果,無論非同步操作結果是正常的還是異常的,都會觸發 handler 的執行。 |
Future |
當非同步操作結果正常時,觸發 handler 的執行。 |
Future |
當非同步操作結果異常時,觸發 handler 的執行。 |
3.4 Future 轉換
Future 轉換是指將一個 Future 物件經過處理之後轉換為另一個 Future 物件。Future 轉換通過 Future 的一系列成員方法來完成,這些轉換 API 的引數是一個 Function, 返回值是另一個 Future 物件。
看下面一段程式碼,根據名字查詢到對應的使用者 User,然後根據使用者 ID 查詢賬戶資訊 Account,再從賬戶中獲取餘額,最後列印餘額。
Future<String> f1 = Future.succeedFuture("Robothy"); // 1
Future<User> f2 = f1.compose(name -> getUserByName(name)); // 2
Future<Account> f3 = f2.compose(user -> getAccountInfo(user.getId())); // 3
Future<Double> f4 = f3.map(account -> account.getBalance()); // 4
f4.onSuccess(balance -> System.out.println("Balance: " + balance)) // 5
.onFailure(cause -> cause.printStacktrace() ); // 6
- 程式碼獲取了一個 SucceedFuture 物件 f1,它的結果是正常訊息 "Robothy"。
- f1 呼叫 compose 方法,設定一個非同步函式,即該行程式碼中的 Lambda 表示式。函式會在 f1 正常完成的時候執行,以非同步的方式根據使用者名稱獲取使用者資訊,並將使用者資訊設定到 f2 中。這行程式碼將 f1 轉化成了 f2。
- f2 繼續呼叫 compose 方法,設定另一個非同步函式,根據使用者 ID 獲取賬戶資訊。同樣,該函式在 f2 正常完成之後執行,執行結果設定到 f3 中,即:把 f2 轉化為 f3。
- f3 呼叫 map 方法,設定一個同步函式,從賬戶資訊中讀取餘額。同步函式在 f3 正常完成之後執行,執行結果會被寫入到 f4 中。
- 為 f4 設定正常訊息處理器,處理器在 f4 正常完成之後被呼叫,列印餘額資訊。
- 為 f4 設定異常處理器,處理器在 f4 異常完成之後被呼叫,列印棧資訊。
為了方便描述,上面程式碼被拆成了多個部分,採用鏈式呼叫可以使程式碼更加簡潔。
Future<String> f1 = Future.succeedFuture("Robothy") // 1
.compose(name -> getUserByName(name)) // 2
.compose(user -> getAccountInfo(user.getId())) // 3
.map(account -> account.getBalance()) // 4
.onSuccess(balance -> System.out.println("Balance: " + balance)) // 5
.onFailure(cause -> cause.printStacktrace() ); // 6
上面提到的同步函式和非同步函式都是 Function 型別,同步函式是同步執行,可以返回任意結果;而非同步函式形式上非同步執行,返回的是一個 Future。之所以說形式上非同步執行,是因為實際上是否非同步取決於實現。例如:getUserByName 的方法簽名如下,但是它方法體的實現可以是同步的,也可以是非同步的。
Future<User> getUserByName(String name);
Future<Account> getAccountInfo(String userId); // 同理
compose 設定的是一個非同步函式,函式被呼叫之後,立即返回一個 Future,但此時非同步函式返回的 Future (區別於 compose 返回的 Future)並不一定已完成。所以並不能向下面這段程式碼一樣,直接讀取結果,正確的做法是像上面一樣通過 compose 將非同步函式連線起來。
Future<User> future = getUserByName(name);
User user = future.result(); // 立即讀取,此時 future 未完成,讀取到的是 null
Future<Account> account = getAccount(user.getId()); // 丟擲 NullPointerException
類似地,map 裡面設定的是同步函式,函式被呼叫之後,返回的結果就是最終結果。getBalance 是 Account 的成員方法,它的方法簽名如下:
Double getBalance();
此外,如果 getUserByName 有異常丟擲,或者它返回的 Future 包含了一個異常訊息,則後續的 compose 和 map 設定的函式都不會被執行。也就是說,只有 2, 6 處設定的函式和處理器會執行。同理,如果 getAccountInfo 發生異常,2,3,6 處的程式碼會被執行;如果呼叫鏈設定的各個函式執行正常,則 2,3,4,5 的函式或處理器會被執行。
這種執行方式與同步程式碼塊中的 try-catch 很相似,假如 getUserByName 和 getAccoutInfo 都是同步的,則程式碼可以用 try-catch 表達如下。
try {
String name = "Robothy"; // 1
User user = getUserByName(name); // 2,注意這裡是同步的,返回的是 User,不是 Future<User>
Account account = getAccountInfo(user.getId()); // 3,返回 Account,不是 Future<Account>
Double balance = account.getBalance(); // 4
System.out.println("Balance: " + balance); // 5
} catch(Throwable cause) {
cause.printStacktrace(); // 6
}
map 和 compose 會在遇到異常時會跳過執行,而 recover 與之相反,只有在遇到異常時才執行。比如把上面程式碼的效果修改一下:如果獲取賬戶餘額的過程中失敗,則預設使用者餘額是 0。程式碼可以這樣寫:
Future<String> f1 = Future.succeedFuture("Robothy")
.compose(name -> getUserByName(name))
.compose(user -> getAccountInfo(user.getId()))
.map(account -> account.getBalance())
.recover(cause -> Future.succeedFuture(0.0)) // 攔截異常,把餘額設定成 0.
.onSuccess(balance -> System.out.println("Balance: " + balance))
.onFailure(cause -> cause.printStacktrace() );
Future 還有其他的轉換操作,這裡不一一贅述,下面會給出 API 清單。
Future 轉換 API 和前面的 Future 結果處理 API 很相似,內部都是將引數包裝成監聽器,都可以鏈式呼叫,區別在於 Future 結果處理 API 返回的是 this,而轉換 API 返回的是另一個 Future 物件。在實際開發中,通常在呼叫鏈的尾部使用處理 API,而在中間使用轉換 API。
Future 操作(成員方法) | 實現類 | 說明 |
---|---|---|
Future compose(Function<T, Future> mapper) | Composition | mapper 是一個非同步操作,將當前 Future 的正常結果轉化為另一個 Future 的非同步結果。當且僅當當前 Future 狀態是 Succeed 的時候呼叫 mapper。 |
Future flatMap(Function<T, Future> mapper) | Composition | 同 compose。 |
Future compose(Function<T, Future> successMapper, Function<Throwable, Future> failureMapper) | Composition | succeedMapper 與 failedMapper 均為非同步操作,分別將當前 Future 的正常結果和異常結果轉化為另一個 Future 的非同步結果。當前 Future 完成狀態是 Succeed 時,執行 succeedMapper;當前 Future 完成狀態是 Failed 時,執行 failureMapper。 |
Future |
Composition | mapper 是一個非同步操作,將當前 Future 的異常結果轉化為另一個 Future 的非同步結果。當且僅當當前 Future 狀態是 Failed 的時候呼叫 mapper。 |
Future transform(Function<AsyncResult |
Transformation | mapper 是一個非同步操作,將當前 Future 的非同步結果轉化為另一個 Future 的非同步結果。 |
Future |
Eventually | mapper 是一個非同步操作,在當前 Future 完成之後執行,返回一個非同步結果。 |
Future map(Function<T, U> mapper) | Mapping | mapper 是一個同步操作,將當前 Future 的正常結果轉化為另一個 Future 的非同步結果。當且僅當當前 Future 狀態是 Succeed 的時候呼叫 mapper。 |
FixedMapping | 忽略當前 Future 的正常結果,value 作為轉化的另一個 Future 的非同步結果。當且僅當當前 Future 狀態是 Succeed 時有效。 | |
Future |
Otherwise | mapper 是一個同步操作,將當前 Future 的異常結果轉化為另一個 Future 的非同步結果。當且僅當當前 Future 狀態是 Failed 的時候呼叫 mapper。 |
Future |
FixedOtherwise | 忽略當前 Future 的異常結果,value 作為轉化的另一個 Future 的非同步結果。當且僅當當前 Future 狀態是 Failed 時有效。 |
4. 小結
以上是 Vert.X Promise/Future 的基本概念和用法。為了方便理解,可以把 Promise 看做生產者,Future 看做消費者。生產者 Promise 即可以生產正常訊息,也可以生產一個異常,消費者 Future 可以消費正常訊息,也可以消費異常。一個 Promise 物件整個生命週期只能夠寫入一次訊息,而對應的 Future 可以消費這條訊息多次。
Future 作為非同步結果(AsyncReuslt)時有狀態,當狀態是成功完成時,可以讀取正常結果(訊息),當狀態是失敗完成時,可以讀取一個異常。SucceedFuture 和 FailedFuture 是兩種特殊的 Future,它們在例項化之後就是已完成狀態。
Future 作為消費者提供了結果處理 API 和 Future 轉換 API。結果處理 API 可以設定處理器 Handler 來處理相應的結果,轉換 API 可以設定函式 Function 把當前 Future 轉化為另一個 Future 物件。
Promise/Future 本身比較抽象,非同步程式設計也是個技術難點。 本文僅僅包含了 Promise/Future 最基本的用法,要熟練掌握它們的用法,寫出高效、優雅的非同步程式碼還需要進行大量的練習。