使用 Vert.X Future/Promise 編寫非同步程式碼

Robothy發表於2022-04-28

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 狀態會發生改變,狀態圖如下所示。

stateDiagram [*] --> INCOMPLETE INCOMPLETE --> SUCCEED: complete() / tryComplete() INCOMPLETE --> FAILED: fail() / tryFail() SUCCEED --> [*] FAILED --> [*]

當狀態是成功完成(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 onComplete(Handler<AsyncResult> handler) 處理一個非同步結果,無論非同步操作結果是正常的還是異常的,都會觸發 handler 的執行。
Future onSuccess(Handler handler) 當非同步操作結果正常時,觸發 handler 的執行。
Future onFailure(Handler handler) 當非同步操作結果異常時,觸發 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
  1. 程式碼獲取了一個 SucceedFuture 物件 f1,它的結果是正常訊息 "Robothy"。
  2. f1 呼叫 compose 方法,設定一個非同步函式,即該行程式碼中的 Lambda 表示式。函式會在 f1 正常完成的時候執行,以非同步的方式根據使用者名稱獲取使用者資訊,並將使用者資訊設定到 f2 中。這行程式碼將 f1 轉化成了 f2。
  3. f2 繼續呼叫 compose 方法,設定另一個非同步函式,根據使用者 ID 獲取賬戶資訊。同樣,該函式在 f2 正常完成之後執行,執行結果設定到 f3 中,即:把 f2 轉化為 f3。
  4. f3 呼叫 map 方法,設定一個同步函式,從賬戶資訊中讀取餘額。同步函式在 f3 正常完成之後執行,執行結果會被寫入到 f4 中。
  5. 為 f4 設定正常訊息處理器,處理器在 f4 正常完成之後被呼叫,列印餘額資訊。
  6. 為 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 recover(Function<Throwable, Future> mapper) Composition mapper 是一個非同步操作,將當前 Future 的異常結果轉化為另一個 Future 的非同步結果。當且僅當當前 Future 狀態是 Failed 的時候呼叫 mapper。
Future transform(Function<AsyncResult, Future> mapper) Transformation mapper 是一個非同步操作,將當前 Future 的非同步結果轉化為另一個 Future 的非同步結果。
Future eventually(Function<Void, Future> mapper) Eventually mapper 是一個非同步操作,在當前 Future 完成之後執行,返回一個非同步結果。
Future map(Function<T, U> mapper) Mapping mapper 是一個同步操作,將當前 Future 的正常結果轉化為另一個 Future 的非同步結果。當且僅當當前 Future 狀態是 Succeed 的時候呼叫 mapper。
Future map(V value) FixedMapping 忽略當前 Future 的正常結果,value 作為轉化的另一個 Future 的非同步結果。當且僅當當前 Future 狀態是 Succeed 時有效。
Future otherwise(Function<Throwable, T> mapper) Otherwise mapper 是一個同步操作,將當前 Future 的異常結果轉化為另一個 Future 的非同步結果。當且僅當當前 Future 狀態是 Failed 的時候呼叫 mapper。
Future otherwise(T value) 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 最基本的用法,要熟練掌握它們的用法,寫出高效、優雅的非同步程式碼還需要進行大量的練習。

相關文章