kotlin coroutine文件:取消和超時
取消和超時
這部分講述協程的取消和超時
取消協程的執行
在一個長期執行的應用,你可能需要細粒度地控制後臺協程。例如,一個使用者可能關閉頁面,這個頁面啟動了一個協程,它的結果現在不再需要了,它的執行應該要取消。launch函式返回一個Job,它可以用來取消執行的協程:
import kotlinx.coroutines.*
fun main() = runBlocking {
//例子開始
val job = launch {
repeat(1000) { i ->
println("I'm sleeping $i ..." )
delay(500L)
}
}
delay(1300L) // 延遲一會兒
println("main: I'm tired of waiting!")
job.cancel() // 取消一個job
job.join() // 等待job結束
println("main: Now I can quit.")
//例子結束
}
在這裡可以獲取完整程式碼
執行結果如下:
I'm sleeping 0 ... I'm sleeping 1 ... I'm sleeping 2 ... main: I'm tired of waiting! main: Now I can quit.
一旦主協程呼叫了job.cancel,我們在其他協程不再能看見任何輸出了,因為這個協程被取消了。有一個Job擴充套件函式cancelAndJoin,它結合了cancel和join呼叫。
取消是協作的
協程取消是協作的(cooperative)。為了可取消,協程程式碼必須是協作的。kotlinx.coroutines裡面所有掛起函式是可取消的。它們檢查協程的可取消性,當被取消時,丟擲CancellationException。然而,如果一個協程正在做計算工作,沒有檢查可取消性,那麼他是不能取消的,就像如下例子所示:
import kotlinx.coroutines.*
fun main() = runBlocking {
//例子開始
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) { // 計算迴圈,僅僅為了浪費CPU
// 一秒列印資訊兩次
if (System.currentTimeMillis() >= nextPrintTime) {
println("I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // 延遲一會兒
println("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消job,等待它結束
println("main: Now I can quit.")
//例子結束
}
在這裡可以獲取完整程式碼
執行它可以發現,它不停列印“I’m sleeping”,即使在取消之後,直至經過五個迭代之後job自己結束。
使得計算程式碼可取消
有兩個方法可以讓計算程式碼可取消。第一個是定期地呼叫一個掛起函式,這個函式檢查可取消。有一個yield函式,是達到這個目的一個很好的選擇。另外一個顯式地檢查可取消狀態。讓我們試試後一種的方法。
用 while (isActive) 代替前面例子中的 while (i < 5),然後從新執行
import kotlinx.coroutines.*
fun main() = runBlocking {
//例子開始
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (isActive) { // 可取消的計算迴圈
// 一秒列印資訊兩次
if (System.currentTimeMillis() >= nextPrintTime) {
println("I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // 延遲一會兒
println("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消job,等待它結束
println("main: Now I can quit.")
//例子結束
}
在這裡可以獲取完整程式碼
就像你看見的,這個迴圈現在取消了,通過CoroutineScope物件,isActive是一個在協程程式碼裡面可以獲取到的擴充套件屬性。
用finally關閉資源
可取消的掛起函式在取消時丟擲CancellationException,可以用一個更加通用的方式處理。例如, 通常在協程取消的時候,try {…} finally {…} 表示式、Kotlin的use函式執行它們的終結任務:
import kotlinx.coroutines.*
fun main() = runBlocking {
//例子開始
val job = launch {
try {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
} finally {
println("I'm running finally")
}
}
delay(1300L) // 延遲一會兒
println("main: I'm tired of waiting!")
job.cancelAndJoin() //取消job,等待它結束
println("main: Now I can quit.")
//例子結束
}
在這裡可以獲取完整程式碼
join和cancelAndJoin兩者都會等待所有終結任務結束,所以上面例子得出如下結果:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
main: I'm tired of waiting!
I'm running finally
main: Now I can quit.
執行非可取消的程式碼塊
前面例子finally程式碼塊中,使用掛起函式的任何嘗試會造成CancellationException,這是因為執行這段程式碼的協程被取消了。通常這不是一個問題,因為所有良好執行的關閉操作(關閉一個檔案、取消一個job,或者關閉任何型別的通訊通道)通常是非阻塞的,不會包含任何掛起函式。然而,極端情況下,當你需要在取消協程中掛起,你可以用使用withContext和NonCancellable上下文,把相關程式碼包含到withContext(NonCancellable) {…}裡面,,就像下面例子所示:
import kotlinx.coroutines.*
fun main() = runBlocking {
//例子開始
val job = launch {
try {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
} finally {
withContext(NonCancellable) {
println("I'm running finally")
delay(1000L)
println("And I've just delayed for 1 sec because I'm non-cancellable")
}
}
}
delay(1300L) // 延遲一會兒
println("main: I'm tired of waiting!")
job.cancelAndJoin() //取消job,等待它結束
println("main: Now I can quit.")
//例子結束
}
在這裡可以獲取完整程式碼
超時
在實踐中,取消協程執行的最明顯理由是,它的執行時間超過了某個時限。雖然你可以手動地追蹤相應Job的引用,然後啟動獨立的協程在一定時限後取消跟蹤的job,但是有一個withTimeout函式,正是處理這件事的:
import kotlinx.coroutines.*
fun main() = runBlocking {
//例子開始
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
withTimeout丟擲的TimeoutCancellationException,是CancellationException的子類。我們從沒有在控制檯上看見列印過它的堆疊。這是因為在一個取消的協程內部,CancellationException被認為是協程結束的一個合理理由。然而這個例子中,我們正是在main函式裡面使用了withTimeout。
因為取消僅僅是一個異常,所有資源是在正常情況下關閉的。你可以在try {…} catch (e: TimeoutCancellationException) {…}程式碼塊中包含超時的程式碼,如果你需要做一些額外的操作,特別是對某種超時或者使用withTimeoutOrNull函式,這個函式類似於withTimeout,但是在超時時返回null而不是丟擲一個異常:
import kotlinx.coroutines.*
fun main() = runBlocking {
//例子開始
val result = withTimeoutOrNull(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
"Done" // 將在產生這個結果之前得到取消
}
println("Result is $result")
//例子結束
}
在這裡可以獲取完整程式碼
當執行這段程式碼時,不再是一個異常:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Result is null