最近一直在了解關(guān)于 Kotlin協(xié)程 的知識,那最好的學(xué)習(xí)資料自然是官方提供的學(xué)習(xí)文檔了六敬,看了看后我就萌生了翻譯官方文檔的想法碘赖。前后花了要接近一個月時間,一共九篇文章外构,在這里也分享出來普泡,希望對讀者有所幫助。個人知識所限典勇,有些翻譯得不是太順暢劫哼,也希望讀者能提出意見
協(xié)程官方文檔:coroutines-guide
本節(jié)討論協(xié)程的取消和超時
一、取消協(xié)程執(zhí)行
在一個長時間運(yùn)行的應(yīng)用程序中割笙,我們可能需要對協(xié)程進(jìn)行細(xì)粒度控制。例如,用戶可能關(guān)閉了啟動了協(xié)程的頁面伤溉,現(xiàn)在不再需要其運(yùn)行結(jié)果般码,此時就應(yīng)該主動取消協(xié)程。launch 函數(shù)的返回值 Job 對象就可用于取消正在運(yùn)行的協(xié)程
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
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.")
//sampleEnd
}
運(yùn)行結(jié)果
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.
只要 main 函數(shù)調(diào)用了 job.cancel
乱顾,我們就看不到 job 協(xié)程的任何輸出了板祝,因?yàn)樗驯蝗∠_€有一個 Job 的擴(kuò)展函數(shù) cancelAndJoin
走净,它結(jié)合了 cancel
和 join
的調(diào)用券时。
cancel() 函數(shù)用于取消協(xié)程,join() 函數(shù)用于阻塞等待協(xié)程執(zhí)行結(jié)束伏伯。之所以連續(xù)調(diào)用這兩個方法橘洞,是因?yàn)?cancel() 函數(shù)調(diào)用后會馬上返回而不是等待協(xié)程結(jié)束后再返回,所以此時協(xié)程不一定是馬上就停止了说搅,為了確保協(xié)程執(zhí)行結(jié)束后再執(zhí)行后續(xù)代碼炸枣,此時就需要調(diào)用 join() 方法來阻塞等待∨螅可以通過調(diào)用 Job 的擴(kuò)展函數(shù) cancelAndJoin() 來完成相同操作
public suspend fun Job.cancelAndJoin() {
cancel()
return join()
}
二适肠、取消操作是協(xié)作完成的
協(xié)程的取消操作是協(xié)作(cooperative)完成的,協(xié)程必須協(xié)作才能取消候引。kotlinx.coroutines
中的所有掛起函數(shù)都是可取消的侯养,它們在運(yùn)行時會檢查協(xié)程是否被取消了,并在取消時拋出 CancellationException 澄干。但是沸毁,如果協(xié)程正在執(zhí)行計(jì)算任務(wù),并且未檢查是否已處于取消狀態(tài)的話傻寂,則無法取消協(xié)程息尺,如以下示例所示:
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
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.")
//sampleEnd
}
運(yùn)行代碼可以看到即使在 cancel 之后協(xié)程 job 也會繼續(xù)打印 "I'm sleeping" ,直到 Job 在迭代五次后(運(yùn)行條件不再成立)自行結(jié)束
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm sleeping 3 ...
job: I'm sleeping 4 ...
main: Now I can quit.
三疾掰、使計(jì)算代碼可取消
有兩種方法可以使計(jì)算類型的代碼可以被取消搂誉。第一種方法是定期調(diào)用一個掛起函數(shù)來檢查取消操作,yieid()
函數(shù)是一個很好的選擇静檬。另一個方法是顯示檢查取消操作炭懊。讓我們來試試后一種方法
使用 while (isActive)
替換前面例子中的 while (i < 5)
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
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.")
//sampleEnd
}
如你所見,現(xiàn)在這個循環(huán)被取消了拂檩。isActive 是一個可通過 CoroutineScope 對象在協(xié)程內(nèi)部使用的擴(kuò)展屬性
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.
四侮腹、用 finally 關(guān)閉資源
可取消的掛起函數(shù)在取消時會拋出 CancellationException,可以用常用的方式來處理這種情況稻励。例如父阻,try {...} finally {...}
表達(dá)式和 kotlin 的 use
函數(shù)都可用于在取消協(xié)程時執(zhí)行回收操作
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
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.")
//sampleEnd
}
join() 和 cancelAndJoin() 兩個函數(shù)都會等待所有回收操作完成后再繼續(xù)執(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.
五、運(yùn)行不可取消的代碼塊
如果在上一個示例中的 finally
塊中使用掛起函數(shù)加矛,將會導(dǎo)致拋出 CancellationException履婉,因?yàn)榇藭r協(xié)程已經(jīng)被取消了(例如,在 finally 中先調(diào)用 delay(1000L) 函數(shù)斟览,將導(dǎo)致之后的輸出語句不執(zhí)行)毁腿。通常這并不是什么問題,因?yàn)樗行阅芰己玫年P(guān)閉操作(關(guān)閉文件苛茂、取消作業(yè)已烤、關(guān)閉任何類型的通信通道等)通常都是非阻塞的,且不涉及任何掛起函數(shù)妓羊。但是胯究,在極少數(shù)情況下,當(dāng)需要在取消的協(xié)程中調(diào)用掛起函數(shù)時侍瑟,可以使用 withContext 函數(shù)和 NonCancellable 上下文將相應(yīng)的代碼包裝在 withContext(NonCancellable) {...}
代碼塊中唐片,如下例所示:
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
val job = launch {
try {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
} finally {
withContext(NonCancellable) {
println("job: I'm running finally")
delay(1000L)
println("job: And I've just delayed for 1 sec because I'm non-cancellable")
}
}
}
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.")
//sampleEnd
}
此時,即使在 finally 代碼塊中調(diào)用了掛起函數(shù)涨颜,其也將正常生效费韭,且之后的輸出語句也會正常輸出
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
job: And I've just delayed for 1 sec because I'm non-cancellable
main: Now I can quit.
六、超時
大多數(shù)情況下庭瑰,我們會主動取消協(xié)程的原因是由于其執(zhí)行時間已超出預(yù)估的最長時間星持。雖然我們可以手動跟蹤對相應(yīng) Job 的引用,并在超時后取消 Job弹灭,但官方也提供了 withTimeout 函數(shù)來完成此類操作督暂。看一下示例:
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
//sampleEnd
}
運(yùn)行結(jié)果:
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
withTimeout 引發(fā)的 TimeoutCancellationException
是 CancellationException 的子類穷吮。之前我們從未在控制臺上看過 CancellationException 這類異常的堆棧信息逻翁。這是因?yàn)閷τ谝粋€已取消的協(xié)程來說,CancellationException 被認(rèn)為是觸發(fā)協(xié)程結(jié)束的正常原因捡鱼。但是八回,在這個例子中,我們在主函數(shù)中使用了 withTimeout
函數(shù)驾诈,該函數(shù)會主動拋出 TimeoutCancellationException
你可以通過使用 try{...}catch(e:TimeoutCancellationException){...}
代碼塊來對任何情況下的超時操作執(zhí)行某些特定的附加操作缠诅,或者通過使用 withTimeoutOrNull
函數(shù)以便在超時時返回 null 而不是拋出異常
import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
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")
//sampleEnd
}
此時將不會打印出異常信息
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Result is null