java程序員的kotlin課(N+1):coroutines 取消和超時(shí)

本文大部分翻譯至:https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html
做了輕微優(yōu)化
為什么翻譯
我知道有一般中文版的文檔,之所以還進(jìn)行翻譯有兩個原因:

  • 看了好幾遍,總是記不住,翻譯一下,加深一下印象
  • 翻譯的版本和原版的英文版,筆者認(rèn)為講的不夠簡單

為什么是N+1
kotlin系列文章照理說應(yīng)該有一系列的文章,協(xié)程絕對應(yīng)該是排在靠后的位置的,但是因?yàn)楣P者最近一直在看這塊的東西,而一些基礎(chǔ)類的kotlin的文章反而沒有寫,所以協(xié)程系列文章以N開始,這是第二篇,所以是N+1

取消協(xié)程執(zhí)行

在長時(shí)間執(zhí)行的應(yīng)用中,你也許需要對后臺運(yùn)行的協(xié)程有合適粒度的控制。舉例來講,比如用戶已經(jīng)關(guān)閉了某一個頁面,那么后臺運(yùn)行的用來加載頁面數(shù)據(jù)的協(xié)程就沒有必要繼續(xù)運(yùn)行了并且應(yīng)該被取消。lanch函數(shù)會返回一個job對象,這個job對象可以用來取消運(yùn)行中的協(xié)程:

val job = launch {
    repeat(1000) { i ->
        println("job: I'm sleeping $i ...")
        delay(500L)
    }
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancel() // cancels the job
job.join() // waits for job's completion 
println("main: Now I can quit.")

這會產(chǎn)生如下的輸出內(nèi)容:

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.

一但主函數(shù)執(zhí)行了job.cancel,我們再也看不到協(xié)程有輸出,因?yàn)樗蝗∠恕ob對象還有另外一個拓展函數(shù)cancelAndJoin,這個函數(shù)會融合cancel和join兩個操作。

取消是需要配合的

協(xié)程的取消是需要內(nèi)外一起配合的(此處有點(diǎn)拗口,后面不按照原文進(jìn)行翻譯了),啥意思呢?就是協(xié)程的取消,需要協(xié)程執(zhí)行體內(nèi)部配合外部的取消信號的;如果熟悉java的線程取消,大家可能會知道,如果是阻塞的方法,被取消會拋出·InterruptedException·,而非阻塞方法如果需要取消,必須在關(guān)鍵節(jié)點(diǎn)進(jìn)行檢查,檢查線程當(dāng)前是否被中斷。協(xié)程也是一樣的,如果一個協(xié)程內(nèi)部沒有suspend的代碼,又沒有在關(guān)鍵點(diǎn)設(shè)立檢查點(diǎn),協(xié)程是無法被取消的。比如下面這段代碼,一個while循環(huán)沒有設(shè)置任何協(xié)程取消檢查點(diǎn),所以在mian函數(shù)里調(diào)用了cancelAndJoin之后,協(xié)程內(nèi)部依然不會停止執(zhí)行。

val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
    var nextPrintTime = startTime
    var i = 0
    while (i < 5) { // computation loop, just wastes CPU
        // print a message twice a second
        if (System.currentTimeMillis() >= nextPrintTime) {
            println("job: I'm sleeping ${i++} ...")
            nextPrintTime += 500L
        }
    }
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")

關(guān)于線程的取消,可以參考一下筆者早些年寫的一篇java 線程池與通過Future終止線程實(shí)例

讓協(xié)程可以被取消

讓協(xié)程可以被取消,有兩種思路:

  • 因?yàn)閟uspending的函數(shù)是可以被取消的,所以定期的調(diào)用一下suspending的方法,用來檢查當(dāng)前協(xié)程是否被取消。有一個函數(shù)yield是用來做這個的一個好選擇
  • 另一個是顯式的檢查被取消的狀態(tài)
    現(xiàn)在讓我們來看下后一種方案:
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
    var nextPrintTime = startTime
    var i = 0
    while (isActive) { // cancellable computation loop
        // print a message twice a second
        if (System.currentTimeMillis() >= nextPrintTime) {
            println("job: I'm sleeping ${i++} ...")
            nextPrintTime += 500L
        }
    }
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")

現(xiàn)在如你所見,這個循環(huán)是可以被取消了。isActive是一個在CoroutineScope內(nèi)部可用的拓展屬性。

通過finally做取消后的善后工作

關(guān)閉suspending的函數(shù),會拋出CancellationException異常,這個異??梢园凑粘R?guī)做法來進(jìn)行處理,比如try {...} finally {...}

val job = launch {
    try {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    } finally {
        println("job: I'm running finally")
    }
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")

join和cancelAndJoin函數(shù)會等待finally動作執(zhí)行完畢,所以上面的例子,會輸出如下:

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm running finally
main: Now I can quit.

在finally里執(zhí)行non-cancellable的代碼

上面的代碼演示了我們可以在finally的代碼塊中執(zhí)行資源回收的操作,但是如果finally的代碼塊中執(zhí)行suspend的代碼會怎樣?

val job = launch(Dispatchers.Default) {
    try {
      while (isActive){
        println("do sth.")
        delay(1000)
      }
    } finally {
      println("do in the finally")
      delay(10000)
      println("finally done.")
    }
  }

  delay(1300L) // delay a bit
  println("main: I'm tired of waiting!")
  job.cancelAndJoin() // cancels the job and waits for its completion
  println("main: Now I can quit.")

輸出如下:

do sth.
do sth.
main: I'm tired of waiting!
do in the finally
main: Now I can quit.

finally done并沒有被打印。
原因:我們的代碼里在main中取消了協(xié)程的執(zhí)行,協(xié)程內(nèi)部的finally里又執(zhí)行了delay方法,這個方法會使協(xié)程進(jìn)入suspending狀態(tài),而協(xié)程被取消時(shí)suspending的執(zhí)行函數(shù)會被取消。
但是因?yàn)閒inally里執(zhí)行的都是需要做掃尾工作的動作,如果被取消,可能會造成資源泄漏問題,解決方案是用過withContext(NonCancellable){...}來包裝掃尾工作的代碼, 讀者可以自己試一下。

超時(shí)

withTimeout(1300L) {
    repeat(1000) { i ->
        println("I'm sleeping $i ...")
        delay(500L)
    }
}

代碼輸出如下:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms

代碼打印了一個異常的堆棧信息,拋出了一個TimeoutCancellationException,它繼承至CancellationException。我們之前在取消一個協(xié)程的時(shí)候,從來沒有看到過console中有打印過這個異常信息,因?yàn)镃ancellationException被認(rèn)為是協(xié)程內(nèi)外用來做配合的常規(guī)異常。當(dāng)然如果一個協(xié)程不是可取消的,那么timeout對它也是無可奈何的,比如下面這段代碼:

withTimeout(1000) {
    launch {
      while (true){
        println(1)
      }
    }
  }
  println("done")

在看下面這段代碼

val result = withTimeoutOrNull(1300L) {
    repeat(1000) { i ->
        println("I'm sleeping $i ...")
        delay(500L)
    }
    "Done" // will get cancelled before it produces this result
}
println("Result is $result")

這段代碼因?yàn)槭褂昧?code>withTimeoutOrNull, 當(dāng)超時(shí)時(shí)并不會拋出異常,而是會返回空。讀者可以自己試一下。

系列文章快速導(dǎo)航:
java程序員的kotlin課(一):環(huán)境搭建
java程序員的kotlin課(N):coroutines基礎(chǔ)
java程序員的kotlin課(N+1):coroutines 取消和超時(shí)
java程序員的kotlin課(N+2):suspending函數(shù)執(zhí)行編排

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容