合著者: Svetlana Isakova
Kotlin Coroutines 1.5.0 已釋出! 以下為新版本帶來的新特性:
- GlobalScope API已被標記為delicate。GlobalScope作為高階的API很容易被濫用。 在可能會被濫用的地方,現在編譯器將發出警告,並要求您在程式中選擇性引入該類。
- JUnit擴充套件。CoroutinesTimeout已可在JUnit5中使用。
- 完善的Channel API。以及針對庫函式新的命名方案,引入了非掛起函式
trySend
和tryReceive
以作為offer
和poll
更好的替代。 - 穩定的Reactive Integrations。我們新增了更多用於將Reactive Streams型別轉換成Kotlin Flow的函式,很多現有的函式和ReactiveContext API已變得穩定。
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中使用。 我們想要確保函式的名稱能夠傳達其行為的資訊。 結果是,我們提出瞭如下內容:
- 常規的掛起方法保持不變,例如,
send
,receive
。 - 封裝了異常的相應非掛起方法始終以”try”為字首:
trySend
和tryReceive
,而非舊的offer
和poll
。 - 新的封裝異常的掛起方法將以”Catching”作為字尾。
讓我們來深入瞭解這些新方法的細枝末節。
Try 函式:send 和 receive 的非掛起版本
一個協程可以向通道傳送一些資訊,而另一個協程可以從該通道接收這個資訊。 send
和 receive
函式都是掛起的。 如果通道已滿並且不能接受新的元素,則 send
掛起其協程,而如果通道中沒有返回元素,則 receive
掛起其協程:
這些函式擁有可在同步程式碼中使用的非掛起形式:offer
和poll
,但已被廢棄,現在推薦使用trySend
和tryReceive
函式。 讓我們討論這種變動的原因。
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
嘗試向已關閉的通道中增加一個元素時,則會丟擲異常。 我們收到了很多抱怨,表示這種行為容易出錯。 這很容易便忘記去捕獲該異常,又您寧可忽略抑或以其他方式去處理,但它會令您的程式崩潰。
新的trySend
和tryReceive
修復了這個問題,並返回了包含更多細節的結果。 每個返回的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 Reactor和RxJava是該領域中最流行的兩個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 的上下文資訊。
所有響應式整合的上下文都是透過訂閱者的上下文隱式傳播的,例如 Mono
,Flux
,Publisher.asFlow
,Flow.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 特定整合的庫。
更多的觀看及閱讀材料
- Kotlin Coroutines 1.5.0 影片
- Coroutines 指南
- API 文件
- Kotlin Coroutines’ GitHub repository
- Coroutines 1.4.0 博文
- Kotlin 1.5.0 釋出博文
如果您遇到任何問題
- GitHub問題跟蹤器報告問題。
- 在Kotlin Slack上的#coroutines頻道中尋求幫助(獲得邀請)。