當希望某段 Coroutine 程式必須在限定時間內完成,可以使用 withTimeoutwithTimeoutOrNull

suspend fun <T> withTimeout(
    timeMillis: Long,
    block: suspend CoroutineScope.() -> T
): T

兩個參數:時間(毫秒),以及要執行的 Coroutine 區塊。

withTimeout 基本用法#

fun main() = runBlocking {
    val job = launch {
        println("inside: ${Thread.currentThread().name}")
        withTimeout(200) {
            repeat(10) {
                println("delay $it times")
                delay(50)
                yield()
            }
            println("withTimeout")
        }
    }
    delay(100)
    println("outer: ${Thread.currentThread().name}")
}

repeat(10) 內每次 delay(50),跑十次需要 500 毫秒;用 withTimeout(200) 包起來後,超過 200 毫秒會把區塊取消。

執行結果:

inside: main
delay 0 times
delay 1 times
outer: main
delay 2 times
delay 3 times

repeat 區塊只跑了四次就被取消,符合 200 毫秒的限制。

TimeoutCancellationException#

withTimeout 在時間到時其實會丟出 TimeoutCancellationException。因為這個例外是 CancellationException 的子類別,預設會被 Coroutine 框架靜靜處理掉;如果要自行處理,可以在外層用 try-catch 攔截:

fun main() = runBlocking {
    val job = launch {
        println("inside: ${Thread.currentThread().name}")
        try {
            withTimeout(200) {
                repeat(10) {
                    println("delay $it times")
                    delay(50)
                    yield()
                }
                println("withTimeout")
            }
        } catch (e: TimeoutCancellationException) {
            println(e.message)
        }
    }
    delay(100)
    println("outer: ${Thread.currentThread().name}")
}

最後會多一行 Timed out waiting for 200 ms

withTimeout 可取消#

withTimeout 與 withContext 一樣,會跟著外層 Coroutine 被取消:

fun main() = runBlocking {
    val job = launch {
        println("inside: ${Thread.currentThread().name}")
        try {
            withTimeout(200) {
                repeat(10) {
                    println("delay $it times")
                    delay(50)
                    yield()
                }
                println("withTimeout")
            }
        } catch (e: TimeoutCancellationException) {
            println(e.message)
        }
    }
    delay(100)
    job.cancel()
    job.join()
    println("outer: ${Thread.currentThread().name}")
}

外層 100 毫秒後呼叫 job.cancel(),連帶把 withTimeout 區塊一起取消,repeat 第三次之後就不會再執行。

withTimeoutOrNull#

withTimeoutOrNull 行為和 withTimeout 幾乎一樣,差別在於:

  • withTimeout 在超時會丟出 TimeoutCancellationException
  • withTimeoutOrNull 在超時會回傳 null;如果在時間內順利執行完,會回傳區塊最後的值。
fun main() = runBlocking {
    val job = launch {
        println("inside: ${Thread.currentThread().name}")
        val timeout = withTimeoutOrNull(600) {
            repeat(10) {
                println("delay $it times")
                delay(50)
                yield()
            }
            return@withTimeoutOrNull 10
        }
        println("timeout= $timeout")
    }
    delay(100)
    println("outer: ${Thread.currentThread().name}")
}

當 timeout 設為 600 毫秒,足以跑完 10 次,最後印出 timeout= 10

把 timeout 改成 200 毫秒:

val timeout = withTimeoutOrNull(200) {
    repeat(10) {
        println("delay $it times")
        delay(50)
        yield()
    }
    return@withTimeoutOrNull 10
}
println("timeout= $timeout")

repeat 跑到第四次就被中斷,最後 withTimeoutOrNull 回傳 null,輸出 timeout= null

小結#

  • 想要限制 Coroutine 區塊的執行時間,用 withTimeoutwithTimeoutOrNull
  • 超時時:withTimeout 丟出 TimeoutCancellationException,可用 try-catch 攔截;withTimeoutOrNull 回傳 null
  • 區塊有回傳值時,withTimeoutOrNull 用起來會比較順手。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10267189