本文大部分翻譯至: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í)行編排