1. 程式人生 > >kotlin coroutine文件:取消和超時

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,它結合了canceljoin呼叫。

取消是協作的

協程取消是協作的(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.")
//例子結束    
}

這裡可以獲取完整程式碼

joincancelAndJoin兩者都會等待所有終結任務結束,所以上面例子得出如下結果:

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,或者關閉任何型別的通訊通道)通常是非阻塞的,不會包含任何掛起函式。然而,極端情況下,當你需要在取消協程中掛起,你可以用使用withContextNonCancellable上下文,把相關程式碼包含到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