Kotlin协程的取消与超时
一、取消协程的执行
main 函数调用了 job.cancel ,就可以取消协程。也可以使 Job 挂起的函数 cancelAndJoin 它合并了对 cancel 以及 join 的调用。
fun main() = runBlocking {
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.")
}
控制台输出:
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.
二、取消是协作的
协程的取消是协作的。⼀段协程代码必须协作才能被取消。所有 kotlinx.coroutines 中的挂起函数都是可被取消的 。
它们检查协程的取消,并在取消时抛出 CancellationException。然而,如果协程正在执行计算任务,并且没有检查取消的话,那么它是不能被取消的。
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("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // 等待⼀段时间
println("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消⼀个作业并且等待它结束
println("main: Now I can quit.")
}
三、使计算代码可取消
有两种方法来使执行计算的代码可以被取消
- 定期调用挂起函数来检查取消。可以用 yield。
- 显式的检查取消状态。
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (isActive) { // ⼀个执行计算的循环,只是为了占用 CPU
// while (i < 5) {
// yield()
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // 等待⼀段时间
println("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消⼀个作业并且等待它结束
println("main: Now I can quit.")
}
四、在 finally 中释放资源
可被取消的挂起函数,在被取消时抛出 CancellationException ,可用try {……} finally {……} 表达式以及 Kotlin 的 use 函数⼀般在协程被取消的时候执行它们的终结动作:
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
} catch (e: CancellationException) {
println("e:${e.message}")
} finally {
println("job: I'm running finoally")
}
}
delay(1300L)
println("main: I'm tired of waiting!")
job.cancelAndJoin()
println("main: Now I can quit.")
}
五、运行不能取消的代码块
所有良好的关闭操作(关闭⼀个文件、取消⼀个作 业、或是关闭任何⼀种通信通道)通常都是非阻塞的,并且不会调用任何挂起函数。然而,在真实的案例中,当你 需要挂起⼀个被取消的协程,你可以将相应的代码包装在 withContext(NonCancellable) {……} 中,并 使⽤ withContext 函数以及 NonCancellable 上下文
fun main() = runBlocking {
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)
println("main: I'm tired of waiting!")
job.cancelAndJoin()
println("main: Now I can quit.")
}
六、超时
使用 withTimeout 函数来设置超时时间
fun main() = runBlocking {
withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
}
控制台会输出
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms
withTimeout 抛出了 TimeoutCancellationException ,它是 CancellationException 的子类。我们之前没有在控制台上看到堆栈跟踪信息的打印。这是因为在被取消的协程中 CancellationException 被认为是协程执行结束的正常原因。
如果你需要做⼀些各类使用超时的特别的额外操作,可以使用类似 withTimeout 的 withTimeoutOrNull 函数,并把这些会超时的代码包装在 try {...} catch (e: TimeoutCancellationException) {...} 代码块中,而 withTimeoutOrNull 通过返回 null 来进行超时操作,从而替代抛出⼀个异常:
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