Kotlin Coroutines 1.5: GlobalScope

kotliner發表於2021-06-13

合著者: Svetlana Isakova

Kotlin Coroutines 1.5.0 已釋出! 以下為新版本帶來的新特性:

  • GlobalScope API已被標記為delicate。GlobalScope作為高階的API很容易被濫用。 在可能會被濫用的地方,現在編譯器將發出警告,並要求您在程式中選擇性引入該類。
  • JUnit擴充套件。CoroutinesTimeout已可在JUnit5中使用。
  • 完善的Channel API。以及針對庫函式新的命名方案,引入了非掛起函式trySendtryReceive以作為offerpoll更好的替代。
  • 穩定的Reactive Integrations。我們新增了更多用於將Reactive Streams型別轉換成Kotlin Flow的函式,很多現有的函式和ReactiveContext API已變得穩定。

開始使用Coroutines 1.5.0

GlobalScope API 已被標記為 delicate

現在 GlobalScope 類已被 @DelicateCoroutinesApi 註解所標記。 從現在開始,所有使用 GlobalScope 的地方都需要 @OptIn(DelicateCoroutinesApi::class) 顯式引入。

雖然大多數情況下都不建議使用 GlobalScope,但官方文件仍透過這個精細的 API 引入這些概念。

全域性 CoroutineScope 不會和任何job繫結。 GlobalScope 多用於啟動執行在整個應用生命週期內且不會提前取消的頂級協程。 在 GlobalScope 中啟動的活動中協程不會讓該程式保持存活狀態。 就像守護執行緒一樣。

源於 GlobalScope 的精密,其使用容易導致資源或記憶體洩露。 從 GlobalScope 中啟動的協程並不遵守結構化併發的原則,一旦因為某種原因(例如較慢的網速)被掛起或延遲,它會持續執行並消耗資源。 例如以下程式碼:

呼叫 loadConfiguration 會在 GlobalScope 中建立一個後臺工作的協程,並且不會等待其取消或完成。 一旦網路很慢,它會一直在後臺等待並且消耗資源。 重複代用 loadConfiguration 將消耗更多的資源。

可行的替代

在許多情況下,應避免使用 GlobalScope,並且其包含的函式應標記為 suspend,例如:

如果透過 GlobalScope.launch 啟動多個併發操作,則應將相關的操作透過 coroutineScope 進行分組:

從頂層程式碼的非掛起上下文中啟動併發操作時,應該使用有所限制的 CoroutineScope 例項來代替 GlobalScope

合理的用法

僅僅有限的場景下,可以合理且安全地使用 GlobalScope,例如必須在整個應用生命週期過程中保持活動的頂層後臺程式。 因此,所有使用 GlobalScope 的地方都需要 @OptIn(DelicateCoroutinesApi::class) 顯式引入,例如:

我們建議您仔細審查所有對 GlobalScope 的用法,並註解那些屬於“合理用例”的用法。 對於其他用法,它們很可能是程式碼中的 bug —— 如上所述來替換掉 GlobalScope 的用法。

JUnit5 擴充套件

我們新增了允許在獨立執行緒中執行測試的 CoroutinesTimeout 註解,並在限制的時間之後讓測試失效並中斷執行緒。 在之前,CoroutinesTimeout 只在 JUnit4 中可用。 在這個正式版中,我們已將其整合到 JUnit5 中了。

要使用這個新的註解,請在您的專案中新增下依賴:

這是一個簡單的示例,說明如何在測試中使用新的 CoroutinesTimeout

在該示例中,CoroutinesTimeout是類級別註解,並且還標記了 firstTest方法。 被註解的測試方法不會超時,因為函式級別的註解覆蓋了類級別的註解。 而secondTest用到了類級別的註解,因此它會超時。

註解以下述形式定義:

第一個引數testTimeoutMs,以毫秒級定義了超時時間。 第二個引數cancelOnTimeout,定義了是否在超時後取消所有正在執行的協程。 如果設定為true,則所有協程將被自動取消。

每當您使用CoroutinesTimeout註解,它會自動啟用協程偵錯程式並在超時時轉儲所有協程。 其轉儲包含了協程建立的堆疊軌跡。 如果有需要禁用對建立的堆疊跟蹤以加快測試速度,請直接使用CoroutinesTimeoutExtension,它允許進行配置。

非常感謝 Abhijit Sarkar,他為 JUnit 5 的 CoroutinesTimeout 構建了一個實用的的 PoC。 這個想法被開發成為我們在1.5版本中新增的 CoroutinesTimeout 新註解。

完善的 Channel API

通道是重要的通訊原語,可讓您可以在不同的協程和回撥之間傳遞資料。 在這個版本中,我們重新設計了部分 Channel API,用更好的選擇來替換了引起混淆的 offer 和 poll 函式。 在這個過程中,我們為掛起和非掛起方法設計了一種新的統一命名方案。

新的命名方案

我們試圖建立統一的命名方案,以便後續在其他庫或Coroutines API中使用。 我們想要確保函式的名稱能夠傳達其行為的資訊。 結果是,我們提出瞭如下內容:

  • 常規的掛起方法保持不變,例如,sendreceive
  • 封裝了異常的相應非掛起方法始終以”try”為字首:trySend 和 tryReceive,而非舊的 offer 和 poll
  • 新的封裝異常的掛起方法將以”Catching”作為字尾。

讓我們來深入瞭解這些新方法的細枝末節。

Try 函式:send 和 receive 的非掛起版本

一個協程可以向通道傳送一些資訊,而另一個協程可以從該通道接收這個資訊。 send 和 receive 函式都是掛起的。 如果通道已滿並且不能接受新的元素,則 send 掛起其協程,而如果通道中沒有返回元素,則 receive 掛起其協程:

這些函式擁有可在同步程式碼中使用的非掛起形式:offerpoll,但已被廢棄,現在推薦使用trySendtryReceive函式。 讓我們討論這種變動的原因。

offer 和 poll 與 send 和 receive 做了相同的事,但不會引發掛起。 這聽起來很簡單,而且在元素可被收發時一切正常。 一旦丟擲異常,會發生什麼呢? send 和 receive 會掛起直到它們能繼續任務。 一旦通道已滿無法新增元素,或通道為空無法檢索任何元素,offer 和 poll 會簡單地返回 false 和 null。 它們都會在關閉了的通道里嘗試執行時丟擲異常,但最後結果卻令人迷惑。

Target platform: JVMRunning on kotlin v.1.5.0

在這個示例中,在元素新增前呼叫 poll 將會立即返回 null。 請注意不該這樣使用:您應該持續定期地輪詢元素,我們是為了簡化說明才直接呼叫。 offer 的呼叫也是不成功的,因為我們的通道是一個 rendezvous 型別的通道,並且其緩衝區容量為零。 結果 offer 返回了 false,而 poll 返回了 null,僅僅是因為它們以錯誤的順序被呼叫。

在上述示例中,取消 channel.close()語句的註釋,將會丟擲異常。 在這個例子中,poll 仍然會像之前一樣返回 false。 但當 offer 嘗試向已關閉的通道中增加一個元素時,則會丟擲異常。 我們收到了很多抱怨,表示這種行為容易出錯。 這很容易便忘記去捕獲該異常,又您寧可忽略抑或以其他方式去處理,但它會令您的程式崩潰。

新的trySendtryReceive修復了這個問題,並返回了包含更多細節的結果。 每個返回的ChannelResult例項,為以下三種其中之一:成功的結果,失敗或通道已關閉的標記。

該示例的工作方式與上一個相同,區別在於 tryReceive 和 trySend 返回了更詳盡的結果。 您可以在輸出中看到 Value(Failed),而不是 false 和 null。 再次取消關閉通道語句的註釋,現在 trySend 會返回 Closed 異常被捕獲的結果。

多虧了內聯值類ChannelResult 不會有額外的封裝類,並且如果返回的是成功值,則原樣返回,而不會產生任何開銷。

異常捕獲函式:帶異常封裝的掛起函式

從這個版本開始,帶異常封裝的掛起方法將以”Catching”作為字尾。 例如,新的 receiveCatching 函式會處理通道關閉情況下的異常。 請思考這個簡單的例子:

在我們嘗試接收值之前通道已被關閉。 但是程式成功執行完畢,表明該通道已關閉。 如果將 receiveCatching 換成普通的 receive 函式,它將丟擲 ClosedReceiveChannelException

目前我們只提供了 receiveCatching 和 onReceiveCatching(而非之前內部的 receiveOrClosed ),但我們打算新增更多函式。

遷移您的程式碼至新函式

您可以用新的快捷執行自動替換專案中 offer 和 poll 函式的所有用法。 由於 offer 返回了布林值,因此其等效替換為 channel.trySend(“ Element”).isSuccess

同樣,poll 函式返回可空的元素,因此將其替換為 channel.tryReceive().getOrNull()

如果返回的結果並沒有被使用,則可以將其直接替換為新的呼叫。

現在對於異常的處理有所不同,因此也許需要您手動進行必要的更新。 如果您的程式碼依賴於 ‘offer’ 和 ‘poll’ 方法在已關閉通道上丟擲的異常,則需要使用以下替代方法。

channel.offer("Element") 的等效替換在已關閉的通道會丟擲異常,即便通道是正常關閉:

如果通道由於錯誤而關閉,則 channel.poll() 的等效替換將引丟擲異常,如果正常關閉,則返回 null

這樣的變動反映了 offer 和 poll 函式舊的行為。

我們假定在大多數情況下,您的程式碼不會依賴於已關閉通道的細微之處,而是依賴它本身是錯誤的根源。 因此,IDE 提供的自動替換功能簡化了語義。 如果這並不符合您的情況,請手動審查並更新,並考慮完全進行重寫,以不同方式處理通道已被關閉的情況,而不是丟擲異常。

響應式整合邁向穩定之旅

對於負責響應式框架整合的大部分函式,Kotlin Coroutines 的 1.5 版本將其升級到穩定 API。

在JVM生態系統,只有少數框架可以處理符合Reactive Streams標準的非同步流。 例如,Project ReactorRxJava是該領域中最流行的兩個Java框架。

Kotlin Flow有所不同,並且其型別與標準指定的不相容,但從概念上來講它們仍然是流。 可將Flow轉換為響應式(規範且相容TCK的)Publisher,反之亦然。 這些開箱即用的轉換器是由kotlinx.coroutines提供的,可以在相應的響應式模組中找到。

例如,如果您需要與Project Reactor型別的互操作性,則應在專案中新增以以下依賴:

然後,如果需要 Reactive Streams 型別,則使用 Flow<T>.asPublisher(),或 Flow<T>.asFlux()(如果您需要 Project Reactor 型別)。

這是該主題的一個概覽。 如果您想了解更多資訊,請考慮閱讀Roman Elizarov’s的Reactive Streams and Kotlin Flows的文章

雖然與響應式庫的整合正朝著API穩定的方向努力,但從技術上講,目標是擺脫 @ExperimentalCoroutinesApi並實現各主題的剩餘內容。

改善與 Reactive Streams 的整合

為了確保第三方框架與 Kotlin Coroutines 之間的互操作性,與 Reactive Streams 規範的相容性很重要。 這對於在老舊專案中無需重寫任何程式碼便能應採用 Kotlin Coroutines 很有幫助。

這次我們設法將大量的函式提升到穩定版。 現在可以將任何 Reactive Streams 型別轉換為 Flow 並返回。 例如新程式碼可以用 Coroutines 編寫,但可以透過反向轉換器與舊的響應式程式碼庫進行整合:

此外,對 ReactorContext 進行了大量的改進,將 Reactor 的 Context 包裝到 CoroutineContext 中,以實現 Project Reactor 與 Kotlin Coroutines 的無縫整合。 透過這個整合,便能使用協程傳遞 Reactor 的上下文資訊。

所有響應式整合的上下文都是透過訂閱者的上下文隱式傳播的,例如 MonoFluxPublisher.asFlowFlow.asPublisher 和 Flow.asFlux。 這是一個將訂閱者的 Context 傳播到 ReactorContext 的簡易示例:

在上面的示例中,我們構造了一個 Flow 例項,然後將其轉換為無上下文的 Reactor 的 Flux 例項。 呼叫不帶引數的 subscribe()方法,其效果是獲取釋出者傳送的所有資料。 結果是程式將列印出“Reactor context in Flow: null”。

下面的鏈式呼叫將 Flow 轉換為 Flux,隨後鏈式呼叫將一個鍵值對 answer = 42 新增到了 Reactor 的上下文中。 對 subscribe() 的呼叫觸發了整個呼叫鏈。 在這種情況下,由於上下文已新增了資料,因此程式將列印”Reactor context in Flow: Context1{answer=42}

新的快捷函式

在 Coroutines 上下文中使用諸如 Mono 之類的響應式型別時,有一些方便的函式可以在不阻塞執行緒的情況下進行檢索。 在這個版本中,我們棄用了所有 Publisher 的 awaitSingleOr* 函式,和專門為 Mono 和 Maybe 設計的 await* 函式。

Mono 最多產生一個值,因此最後一個元素與第一個元素相同。 在這種情況下,刪除剩餘元素的語義是沒有用的。 因此 Mono.awaitFirst() 和 Mono.awaitLast() 被棄用,取而代之的是 Mono.awaitSingle()。

開始使用 kotlinx.coroutines 1.5!

新版本有著令人印象深刻的變動列表。 在完善 Channels API 的同時開發新的命名方案是團隊的一項顯著成就。 同時我們非常注重讓 Coroutines API 儘可能簡單直觀。

要開始使用 Kotlin Coroutines 的新版本,只需更新 build.gradle.kts 檔案的內容。 首先,請確保您擁有最新版本的 Kotlin Gradle 外掛:

然後更新依賴的版本,包括 Reactive Streams 特定整合的庫。

更多的觀看及閱讀材料

如果您遇到任何問題

相關文章