0%

【文章翻譯】Better isolate management with Isolate.run()

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

使用 Isolate.run() 更好地管理 Isolate

Dart 2.19 引入了一個新函數,可以讓您僅用一行代碼即可輕鬆實現併發。

展示新 Isolate.run() 函數速度的基準測試

所有 Dart 程式碼都在 isolate 中運行。是否要實作多個 isolate 以在 Dart 程式中啟用 併發 取決於您。如何實作多個 isolate 則取決於 Dart 團隊,在 Dart 2.19 中,我們對此流程進行了巨大的升級,我們很興奮能與大家分享。來認識一下 Isolate.run() 吧!

run() 將設定和管理 isolate 的所有複雜性完全抽象成單個函數調用。使用一些基本元素來使用 isolate 已經有一段時間了。但是,即使 isolate 有了所有最近的效能改進,這個過程充其量也只是繁瑣的,最糟糕的情況下則容易出錯。

為了讓您了解 run() 的改進程度,本文將逐步分解先前使用低階基本元素建構功能的方法。然後,我們將其與使用 Isolate.run() 進行對比,並向您展示它在內部的運作方式。即使您以前從未使用過 isolate,我們也相信 run() 會讓您興奮地想要嘗試它們!

Isolate

Isolate 是一個相當簡單的概念。Isolate 本質上是 Dart 中的單個執行緒。它們讓您可以並行執行部分程式碼。您可以啟動新的並行執行(數量不限),並直接從 main(主執行緒或 main isolate)告訴它們要做什麼。Isolate 不共享記憶體;相反,它們透過來回傳遞訊息來進行通訊。因此,您不必擔心典型的多執行緒問題,例如競爭條件或互斥鎖和鎖。

聽起來很棒!但是如何使用它們呢?在 Isolate.run() 之前,事情變得棘手起來。

Isolate API 由低階基本元素組成,這些基本元素提供了廣泛的功能。當您需要自訂 isolate 的功能時,這種粒度非常棒。但是,當 必須 使用 isolate 時,這種粒度就不那麼好了。特別是因為幾乎所有 isolate 的使用案例都需要相同的設定和管理基本配置。這基本上意味著要將每個實作細節都公開給 Dart 的使用者自行處理。

讓我們來看看典型的 isolate 設定,以更好地理解 Isolate.run() 所解決的繁瑣過程。

使用 Isolate(之前)

您可以將 Isolate.spawn() 視為 isolate 的舊起點。Flutter 的 compute 函數就是建立在 spawn() 之上的。它接受一個方法作為其入口點參數,以及該方法的任何參數,以及 isolate 本身的其他配置。過去,此入口點只能是頂級或靜態方法。

1
Isolate.spawn(_readAndParseJson, filename);

Isolate 建立完成!開玩笑的。還差得遠呢。

調用 spawn 不會返回任何實際可用的東西。它只返回一個 isolate 物件,該物件只是確認 isolate 已啟動。

除了建立時傳遞的初始參數之外,main isolate 和 spawned isolate(由 spawn 建立的 isolate)無法直接通訊。實際上,即使您不需要從 spawned isolate 返回任何計算結果,您仍然需要某種驗證來確認計算成功,因此您總是需要返回一條訊息。

為了啟用通訊,您必須設定埠。在調用 spawn 之前,您需要建立一個 ReceivePort 物件。ReceivePort 物件的 sendPort 成員作為 spawn 的另一個參數傳遞給 spawned isolate。

這意味著您傳遞給 spawn 的函數必須 專門 配置為使用該 sendPort 執行某些操作。換句話說,您不能僅將現有函數與 isolate 一起使用。因此,您不要將僅讀取和解析 JSON 檔案的常規函數傳遞給 spawn,而是建立如下內容:

您的專用、isolate 友好型 JSON 解碼函數可以簡單地「返回結果」,然後就完成了,對嗎?不完全是。結果需要透過 responsePort 傳送。這就是 isolate 與埠通訊的方式。您可以使用另一個基本元素 Isolate.exit() 來有效地返回結果並同時關閉 spawned isolate:

1
Isolate.exit(responsePort, result);

exit() 函數將儲存 spawned isolate 中訊息的記憶體 傳輸 到 main isolate(而不是複製它),並安全地關閉 isolate。

讓我們把這些都串在一起。由於此範例中的 result 是解析的 JSON,您可能想要稍微解構它才能實際使用它。為了程式碼的簡潔性,我們應該將建立 receivePort 和 isolate 並等待它們的回應的三行程式碼放在它們自己的函數中,而不是直接放在 main() 中。

正在完成的工作相對簡單。正是實作細節的暴露讓它 感覺 起來很複雜,例如用於訊息傳遞的埠,以及需要一個專用函數來處理埠,而該函數與 isolate 無關。

錯誤處理

到目前為止的範例 仍然 不是一個真正的「完整」、可投入生產的實作。如果您不做任何錯誤處理,那將對您自己不利,但它通常在已經相當多的流程中被遺忘為一個額外的步驟。例如,如果沒有任何錯誤處理,未捕獲的異步錯誤導致 isolate 崩潰,您將不知道導致錯誤的原因,甚至不知道 發生了什麼事

涵蓋 isolate 的所有錯誤處理可能性將會很廣泛,但通常需要對程式碼進行一些額外的補充。

您至少可以將 errorsAreFatalonExitonError 參數加入到 spawn 調用中:

這可以確保即使 spawned isolate 在沒有傳送結果的情況下終止,或者發生任何未捕獲的錯誤,resultPort 都會收到一條訊息。將錯誤設定為致命意味著未捕獲的錯誤會退出 isolate 作為安全預防措施,以確保它一定會終止。

onExit 參數使 isolate 在退出時向埠傳送 nullonError 參數使未捕獲的錯誤向埠傳送兩個字串的列表(錯誤和堆疊追蹤的 toString)。

重複使用結果埠可以避免建立更多埠,因此您只需在一个地方查找訊息。但這也意味著您需要區分 onExitonError 訊息與結果值。在這裡,我們假設 JSON 必須是一個 Map,因此它不能是列表或 null。否則,您還必須將結果包裝在可以識別的內容中。您必須在埠訊息之上建立一個(微不足道的)訊息協定。

除此之外,您還可以檢查回應中的特定錯誤。其中一種情況是檢查 resultPort 是否為 null,這意味著 isolate 在沒有傳送結果的情況下終止:

另一種情況是檢查結果是否為列表,這意味著發生了未捕獲的錯誤:

然後,最後,處理實際的結果:

無論如何,您都希望將 spawn 放在 try 塊中,以檢查將入口點傳送到新的 isolate 是否失敗。如果失敗了,結果埠將不會收到任何訊息,並且需要關閉:

提供最基本的錯誤處理可以確保結果埠始終關閉,並且 *spawnAndReceieve 始終完成,無論 spawned isolate 如何退出。您還可以做得 更好,例如,透過捕獲錯誤和堆疊追蹤並將它們作為實際物件傳送回去,而不僅僅是像 onError 處理程式那樣的字串。

錯誤處理顯然會引入很多變化,以及決定如何處理它以及要考慮哪些因素的心理開銷。可以理解的是,它通常被忽略在基本 isolate 設定之外。

使用 Isolate(之後)

Isolate.run() 使用您以前必須自己使用的基本元素,在單個函數調用中設定 isolate 實作的所有部分:

沒有埠、沒有單獨的生成、退出或錯誤處理,也沒有特殊的返回結構。也許最好的部分是,您傳遞給 run 的入口點可以是任何現有函數:

此範例顯示了一個 異步 函數,但 run 可以同樣輕鬆地執行 同步 函數。run 函數本身始終異步返回,這才是最重要的。

入口點也可以是 函數表達式,直接在您調用 run 的位置內聯編寫。Isolate 和在其上編寫的任何更高級別的 API 不再局限於僅運行靜態或頂級函數。

不再需要額外的訊息參數,您可以避免在列表等資料結構中打包和解包參數。

您根本不需要考慮太多錯誤處理。run 函數結合了本地和遠端錯誤捕獲、處理和跨 isolate 通訊,並將結果公開為單個普通的(異步)錯誤,您可以在標準的 try/catch 中捕獲它。您可以忘記 isolate,並將其視為普通的函數。

Isolate.run() 可以讓程式碼更加簡潔和符合人體工程學。Flutter 的 compute 函數甚至也轉而使用 run 而不是 spawn

Isolate.run() 內部

看看 run 的實作。它深入研究了所有與 isolate 相關的低階 API(這在以前是您的工作),以構建一個「完美」、全面的 isolate 設定。它接受要執行的 computation 方法,並設定所有埠及其返回值,以考慮 isolate 之間的有效訊息傳遞。

對每個潛在情況都進行了 徹底的 錯誤處理。run 函數會檢查 isolate 是否在完成計算之前就已終止。如果計算拋出錯誤,則 isolate 將終止,並將相同的錯誤拋出到 main isolate。

如果發生未捕獲的異步錯誤,則 isolate 將終止,並將錯誤異步報告給 main isolate。如果 main isolate 首先終止,則 spawned isolate 將終止,並將情況視為未捕獲的異步錯誤。

最後,run 總是使用 exit 來安全地關閉。這意味著資料可以有效地在 isolate 之間傳輸,而無需實際複製它。

總結

run 函數非常適合啟動一個計算並等待結果。如果您想為 run 未涵蓋的內容(例如可以多次傳送和接收訊息的 長時間運行的 isolate)建構自己的 isolate 設定,則基本元素仍然可用。但是,在大多數情況下,應該使用單個 run 語句替換 spawn 及其所有支援配置,而不是任何其他配置。

如果您在 run 之前從未嘗試過 isolate 管理,那麼很難相信所有這些功能以前都必須由使用者來實作!Isolate.run()(在 Dart 2.19 和 Flutter 3.7 中可用)使程式碼更加符合人體工程學,並使 isolate 更易於使用。您將如何利用 run 節省的時間呢?


使用 Isolate.run() 更好地管理 Isolate 最初發佈在 Medium 的 Dart 上,人們在那裡透過突出顯示和回應這個故事來繼續討論。