首頁/ 遊戲/ 正文

JEP 428:結構化併發,簡化 Java 多執行緒程式設計

作者 | A N M Bazlur Rahman

譯者 | 明知山

策劃 | 丁曉昀

JEP 428,即結構化併發 (孵化器階段),已經從 Proposed 狀態進入到 Target 狀態。在 Project Loom 的框架下,這個 JEP 提議引入一個庫,將在不同執行緒中執行的多個任務視為原子操作,以此來簡化多執行緒程式設計。它可以簡化錯誤處理和取消操作,提高可靠性,並增強可觀察性。這個 API 仍然在孵化當中。

開發人員可以使用 StructuredTaskScope 類來組織他們的併發程式碼,這個類將把一組子任務視為一個單元。子任務透過單獨的執行緒建立,然後連線成一個單元,也可以作為一個單元進行取消。子任務的異常或執行結果將由父任務進行聚合和處理。讓我們來看一個例子:

Response

handle

() throws ExecutionException, InterruptedException

{

try

var

scope =

new

StructuredTaskScope。ShutdownOnFailure()) {

Future

user = scope。fork(() -> findUser());

Future

order = scope。fork(() -> fetchOrder());

scope。

join

();

// 連線

scope。throwIfFailed();

// 丟擲錯誤

// 聚合結果

return

new

Response(user。resultNow(), order。resultNow());

}

上面的 handle() 方法表示伺服器應用程式的一個任務。它建立了兩個子任務來處理傳入的請求。與 ExecutorService。submit() 一樣,StructuredTaskScope。fork() 接受 Callable 作為引數,並返回 Future。與 ExecutorService 不同的是,返回的 Future 不是透過 Future。get() 來連線的。這個 API 執行在 JEP 425 之上——虛擬執行緒 (預覽階段),釋出目標也為 JDK 19。

上面的例子使用了 StructuredTaskScope API,如果要在 JDK 19 上執行它們,必須新增 jdk。incubator。concurrent 模組,同時要啟用預覽功能來使用虛擬執行緒。

使用下面的命令編譯上述程式碼:

javac ——release 19 ——enable-preview ——add-modules jdk。incubator。concurrent Main。java

執行程式也需要提供相同的標誌:

java ——enable-preview ——add-modules jdk。incubator。concurrent Main

不過,我們也可以使用原始碼啟動器直接執行它,命令應該是這樣的:

java ——source 19 ——enable-preview ——add-modules jdk。incubator。concurrent Main。java

jshell 也是可用的,但也需要啟用預覽功能:

jshell ——enable-preview ——add-modules jdk。incubator。concurrent

結構化併發帶來了很多好處。它為呼叫者方法及其子任務建立了一種父子關係。例如,在上面的例子中,handle() 任務是父,它的子任務 findUser() 和 fetchOrder() 是子。結果,整個程式碼塊變成了原子程式碼。它透過執行緒轉儲中的任務層次結構來提供可觀察性。它還可以在錯誤處理中實現短路,如果其中一個子任務失敗,其他未完成的任務將被取消。如果父任務的執行緒在 join() 呼叫之前或期間被中斷,兩個分支將在作用域退出時自動取消。這讓併發程式碼的結構變得更加清晰,開發人員現在可以推理和跟蹤程式碼,就好像它們是在單執行緒環境中執行。

早期的程式流程普遍使用 GOTO 語句來控制,程式碼十分混亂,這種義大利麵條式的程式碼難以閱讀和除錯。隨著程式設計正規化的成熟,程式設計社群認識到 GOTO 語句是有害的。1969 年,以《計算機程式設計的藝術》一書而聞名的計算機科學家 Donald Knuth 表示,沒有 GOTO 也可以高效地編寫程式。後來,結構化程式設計的出現解決了所有這些缺點。看一下下面的例子:

Response handle() throws IOException {

String theUser = findUser();

int theOrder = fetchOrder();

return new Response(theUser, theOrder);

}

上面的程式碼是結構化程式碼的一個例子。在單執行緒環境中,handle() 方法被呼叫時將按順序執行。fetchOrder() 方法不會在 findUser() 方法之前啟動。如果 findUser() 方法失敗,下面的方法根本不會啟動,handle() 方法將隱式失敗,這反過來確保了原子操作成功或不成功。它提供了 handle() 方法及其子方法之間的父子關係,遵循錯誤傳播的規則,並在執行時提供呼叫堆疊資訊。

然而,這種方法和推理並不適用於我們當前的執行緒程式設計模型。例如,如果我們想用 ExecutorService 改寫上述的程式碼,就像這樣:

Response handle() throws ExecutionException, InterruptedException {

Future

user = executorService。submit(() -> findUser());

Future

order = executorService。submit(() -> fetchOrder());

String theUser = user。get(); // 連線findUser

int theOrder = order。get(); // 連線fetchOrder

return new Response(theUser, theOrder);

}

ExecutorService 中的子任務獨立執行,可能成功或失敗。即使父任務被中斷,中斷也不會被傳播到子任務,因此會造成洩漏。它沒有了父關係。由於父任務和子任務將出現線上程轉儲不相關的執行緒呼叫堆疊上,因此除錯也變得困難。儘管程式碼看起來具有邏輯結構,但這種結構只停留在開發人員的頭腦中,而不是在執行過程中。所以,它們是非結構化的併發程式碼。

透過觀察非結構化併發程式碼存在的這些問題,Martin Sústrik 在他的博文中創造了“結構化併發”這個術語,然後 Nathaniel J。 Smith 在他關於結構化併發的文章中推廣了這個術語。關於結構化併發,Oracle 技術諮詢成員、Loom 專案負責人 Ron Pressler 在 InfoQ 的一個播客中說道:

結構化的意思是,如果你生成了什麼東西,你必須等待並連線它。這裡的“結構”與它在結構化程式設計中的含義相似。程式碼的塊結構反映了程式的執行時行為。因此,就像結構化程式設計提供了順序控制流保證,結構化併發也為併發提供了同樣的保證。有興趣深入瞭解結構化併發及其背景故事的開發者可以收聽 InfoQ 的部落格,或者觀看 Ron Pressler 在YouTube上的分享以及Inside Java的文章。

開啟App看更多精彩內容

相關文章

頂部