取消与超时
取消允许你在协程完成之前停止它. 它可以停止不再需要的工作, 例如当协程仍在运行时, 用户关闭了窗口, 或者在用户界面中导航离开. 你也可以使用它来提前释放资源, 以及阻止协程在对象销毁后继续访问它们.
取消通过 Job 句柄工作, Job 表示协程的生命周期, 以及它的父子关系. Job 允许你检查协程是否处于活跃状态, 并允许你取消它, 以及按照 结构化并发 定义的子协程.
取消协程
在协程的 Job 句柄上调用 cancel() 函数时, 协程会被取消. 协程构建器函数, 例如 .launch()), 会返回一个 Job. .async() 函数返回一个 Deferred, 它实现了 Job, 并支持相同的取消行为.
你可以手动调用 cancel() 函数, 这个函数也可以在父协程被取消时, 通过取消的传播, 自动调用.
当协程被取消时, 它在下次检查取消时会抛出 CancellationException. 关于这个异常何时发生以及如何发生, 详情请参见 挂起点(Suspension Point)与取消.
下面是一个示例, 演示如何手动取消协程:
import kotlinx.coroutines.*
import kotlin.time.Duration
//sampleStart
suspend fun main() {
withContext(Dispatchers.Default) {
// 用作信号, 表示协程已开始运行
val job1Started = CompletableDeferred<Unit>()
val job1: Job = launch {
println("The coroutine has started")
// 完成 CompletableDeferred, 表示协程已开始运行
job1Started.complete(Unit)
try {
// 无限期挂起
// 如果没有取消, 这个调用永远不会返回
delay(Duration.INFINITE)
} catch (e: CancellationException) {
println("The coroutine was canceled: $e")
// 始终重新抛出取消异常!
throw e
}
println("This line will never be executed")
}
// 在取消 job1 之前, 等待它启动
job1Started.await()
// 取消协程, delay() 会抛出 CancellationException
job1.cancel()
// async 返回一个 Deferred 句柄, 它继承自 Job
val job2 = async {
// 如果协程在其代码体开始执行之前被取消,
// 则这一行可能不会打印
println("The second coroutine has started")
try {
// 等同于 delay(Duration.INFINITE)
// 挂起, 直到这个协程被取消
awaitCancellation()
} catch (e: CancellationException) {
println("The second coroutine was canceled")
throw e
}
}
job2.cancel()
}
// 协程构建器, 例如 withContext() 或 coroutineScope(),
// 会等待所有子协程完成, 即使子协程被取消
println("All coroutines have completed")
}
//sampleEnd
在这个示例中, CompletableDeferred 用作信号, 表示协程已开始运行. 协程在开始执行时调用 complete(), 而 await() 直到 CompletableDeferred 完成后才返回. 这样, 取消只在协程开始运行后才发生. 由 .async() 创建的协程没有这个检查, 因此它可能在代码块内的代码运行之前就被取消.
取消的传播
结构化并发 会确保取消一个协程时也会取消它的所有子协程. 这可以防止父协程已经停止后子协程继续工作.
下面是一个示例:
import kotlinx.coroutines.*
import kotlin.time.Duration
//sampleStart
suspend fun main() {
withContext(Dispatchers.Default) {
// 用作信号, 表示子协程已启动
val childrenLaunched = CompletableDeferred<Unit>()
// 启动两个子协程
val parentJob = launch {
launch {
println("Child coroutine 1 has started running")
try {
awaitCancellation()
} finally {
println("Child coroutine 1 has been canceled")
}
}
launch {
println("Child coroutine 2 has started running")
try {
awaitCancellation()
} finally {
println("Child coroutine 2 has been canceled")
}
}
// 完成 CompletableDeferred, 表示子协程已启动
childrenLaunched.complete(Unit)
}
// 等待父协程发出信号, 表示它已启动所有子协程
childrenLaunched.await()
// 取消父协程, 这会取消它的所有子协程
parentJob.cancel()
}
}
//sampleEnd
在这个示例中, 每个子协程使用 finally 代码块, 因此其中的代码会在协程被取消时运行. 这里, CompletableDeferred 信号表示子协程在被取消之前已经启动, 但不保证它们已经开始运行. 如果它们先被取消, 则不会打印任何内容.
让协程响应取消
在 Kotlin 中, 协程的取消是 协作式的. 也就是说, 协程只有在通过 挂起 或 明确的检查取消 来协作时, 才能响应取消.
在本节中, 你将学习如何创建可取消的协程.
挂起点(Suspension Point)与取消
当协程被取消时, 它会继续运行, 直到到达代码中可能挂起的位置, 也称为 挂起点(Suspension Point). 如果协程在这里挂起, 挂起函数会检查协程是否已被取消. 如果已被取消, 协程辉停止, 并抛出 CancellationException 异常.
对 suspend 函数的调用是一个挂起点, 但它并不总是挂起. 例如, 当等待 Deferred 结果时, 只有在这个 Deferred 还没有完成时, 协程才会挂起.
下面的示例, 使用了常用的挂起函数, 这些函数会挂起, 使协程能够检查取消, 并在已取消时停止:
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.channels.Channel
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration
suspend fun main() {
withContext(Dispatchers.Default) {
val childJobs = listOf(
launch {
// 挂起, 直到被取消
awaitCancellation()
},
launch {
// 挂起, 直到被取消
delay(Duration.INFINITE)
},
launch {
val channel = Channel<Int>()
// 挂起, 等待一个永远不会发送的值
channel.receive()
},
launch {
val deferred = CompletableDeferred<Int>()
// 挂起, 等待一个永远不会完成的值
deferred.await()
},
launch {
val mutex = Mutex(locked = true)
// 挂起, 等待一个永远保持锁定的互斥锁
mutex.lock()
}
)
// 给子协程时间启动并挂起
delay(100.milliseconds)
// 取消所有子协程
childJobs.forEach { it.cancel() }
}
println("All child jobs completed!")
}
明确的检查取消
如果协程长时间不 挂起, 除非它明确的检查取消, 否则被取消时不会停止.
要检查取消, 请使用以下 API:
isActive 属性, 如果协程被取消, 则为 false.
ensureActive() 函数, 如果协程被取消, 立即抛出 CancellationException.
yield() 函数, 挂起协程, 释放线程, 并给其他协程在这个线程上运行的机会. 挂起协程让它能够检查取消, 如果被取消, 则抛出 CancellationException.
如果你的协程在挂起点之间长时间运行, 或者不太可能在挂起点挂起, 这些 API 非常有用.
isActive
在长时间运行的计算中, 请使用 isActive 属性, 定期检查取消. 当协程不再活跃时, 这个属性为 false, 当协程不再需要继续操作时, 你可以使用这个属性优雅的停止协程:
下面是一个示例:
import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.milliseconds
import kotlin.random.Random
//sampleStart
suspend fun main() {
withContext(Dispatchers.Default) {
val unsortedList = MutableList(10) { Random.nextInt() }
// 启动一个长时间运行的计算
val listSortingJob = launch {
var i = 0
// 在协程保持活跃时, 反复对列表排序
while (isActive) {
unsortedList.sort()
++i
}
println(
"Stopped sorting the list after $i iterations"
)
}
// 对列表排序 100 毫秒, 然后认为排序已经足够好
delay(100.milliseconds)
// 当结果足够好时, 取消排序
listSortingJob.cancel()
// 在访问共享列表之前, 等待排序协程结束, 以避免数据竞争
listSortingJob.join()
println("The list is probably sorted: $unsortedList")
}
}
//sampleEnd
在这个示例中, join() 函数挂起协程, 直到它结束. 这样可以确保排序协程仍在运行时不会访问列表.
ensureActive()
使用 ensureActive() 函数检查取消, 如果协程已被取消, 抛出 CancellationException, 停止当前计算:
import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.milliseconds
suspend fun main() {
withContext(Dispatchers.Default) {
val childJob = launch {
var start = 0
try {
while (true) {
++start
// 检查当前数字的 Collatz 猜想
var n = start
while (n != 1) {
// 如果协程已被取消, 则抛出 CancellationException
ensureActive()
n = if (n % 2 == 0) n / 2 else 3 * n + 1
}
}
} finally {
println("Checked the Collatz conjecture for 0..${start-1}")
}
}
// 运行计算 100 毫秒
delay(100.milliseconds)
// 取消协程
childJob.cancel()
}
}
yield()
yield() 函数挂起协程, 并在恢复前检查取消. 如果不挂起, 同一线程上的协程会顺序运行.
使用 yield, 可以在一个协程完成之前, 允许其他协程在同一线程或线程池上运行:
import kotlinx.coroutines.*
//sampleStart
fun main() {
// runBlocking 使用当前线程运行所有协程
runBlocking {
val coroutineCount = 5
repeat(coroutineCount) { coroutineIndex ->
launch {
val id = coroutineIndex + 1
repeat(5) { iterationIndex ->
val iteration = iterationIndex + 1
// 暂时挂起, 给其他协程运行的机会
// 如果没有机制, 所有的协程会顺序运行
yield()
// 打印协程索引和迭代索引
println("$id * $iteration = ${id * iteration}")
}
}
}
}
}
//sampleEnd
在这个示例中, 每个协程使用 yield(), 让其他协程在迭代之间运行.
在协程被取消时, 中断阻塞代码
在 JVM 上, 某些函数, 例如 Thread.sleep() 或 BlockingQueue.take(), 可以阻塞当前线程. 这些阻塞函数可以中断, 这样就能提前停止它们. 但是, 当从协程中调用它们时, 取消不会中断线程.
要在取消协程时中断线程, 请使用 runInterruptible() 函数:
import kotlinx.coroutines.*
//sampleStart
suspend fun main() {
withContext(Dispatchers.Default) {
val childStarted = CompletableDeferred<Unit>()
val childJob = launch {
try {
// 取消会触发线程中断
runInterruptible {
childStarted.complete(Unit)
try {
// 阻塞当前线程很长时间
Thread.sleep(Long.MAX_VALUE)
} catch (e: InterruptedException) {
println("Thread interrupted (Java): $e")
throw e
}
}
} catch (e: CancellationException) {
println("Coroutine canceled (Kotlin): $e")
throw e
}
}
childStarted.await()
// 取消协程, 并中断运行 Thread.sleep() 的线程
childJob.cancel()
}
}
//sampleEnd
取消协程时, 安全的处理值
当一个挂起的协程被取消时, 它会恢复运行, 并抛出 CancellationException 异常, 而不是返回任何值, 即使这些值已经可用. 这种行为称为 立即取消(Prompt Cancellation). 它能够防止你的代码在已取消的协程作用域中继续执行, 例如更新已经关闭的屏幕.
下面是一个示例:
import java.nio.file.*
import java.nio.charset.*
import kotlinx.coroutines.*
import java.io.*
// 定义一个协程作用域, 它使用 UI 线程
class ScreenWithFileContents(private val scope: CoroutineScope) {
fun displayFile(path: Path) {
scope.launch {
val contents = withContext(Dispatchers.IO) {
Files.newBufferedReader(
path, Charset.forName("US-ASCII")
).use {
it.readLines()
}
}
// 在这里可以安全的调用 updateUi,
// 如果取消, withContext() 不会返回任何值
updateUi(contents)
}
}
// 如果在用户离开屏幕后调用, 会抛出异常
private fun updateUi(contents: List<String>) {
contents.forEach { line -> addOneLineToUi(line) }
}
private fun addOneLineToUi(line: String) {
// 这里需要向 UI 添加一行内容
}
// 只能从 UI 线程调用
fun leaveScreen() {
// 离开屏幕时取消作用域
// 你不能再更新 UI 了
scope.cancel()
}
}
在这个示例中, withContext(Dispatchers.IO) 与取消协作, 如果 leaveScreen() 函数在协程返回文件内容之前就取消了协程, 则防止 updateUI() 运行.
虽然立即取消能够防止在值不再有效后使用它们, 但它也可能在重要的值仍在使用时停止你的代码, 这可能导致丢失这些值. 如果协程接收到一个值, 例如 AutoCloseable 资源, 但在执行关闭它的代码之前, 协程被取消, 就会发生这种情况. 为了防止这种情况, 请将清理逻辑放在, 即使接收值的协程被取消时, 也确保能够运行的地方.
下面是一个示例:
import java.nio.file.*
import java.nio.charset.*
import kotlinx.coroutines.*
import java.io.*
// scope 是使用 UI 线程的协程作用域
class ScreenWithFileContents(private val scope: CoroutineScope) {
fun displayFile(path: Path) {
scope.launch {
// 将 reader 保存在变量中, 让 finally 代码块能够关闭它
var reader: BufferedReader? = null
try {
withContext(Dispatchers.IO) {
reader = Files.newBufferedReader(
path, Charset.forName("US-ASCII")
)
}
// 在 withContext() 完成后, 使用保存的 reader
updateUi(reader!!)
} finally {
// 即使协程被取消, 也确保关闭 reader
reader?.close()
}
}
}
private suspend fun updateUi(reader: BufferedReader) {
// 显示文件内容
while (true) {
val line = withContext(Dispatchers.IO) {
reader.readLine()
}
if (line == null)
break
addOneLineToUi(line)
}
}
private fun addOneLineToUi(line: String) {
// 这里需要向 UI 添加一行内容
}
// 只能从 UI 线程调用
fun leaveScreen() {
// 离开屏幕时取消作用域
// 你不能再更新 UI 了
scope.cancel()
}
}
在这个示例中, 将 BufferedReader 保存在变量中, 并在 finally 代码块中关闭它, 这样可以确保, 即使协程被取消, 也会释放资源.
运行不可取消的代码块
你可以防止取消影响协程的某些部分. 方法是, 将 NonCancellable 作为参数, 传递给 withContext() 协程构建器函数.
即使协程在结束之前被取消, 如果你需要确保某些操作必须完成, 例如使用挂起的 close() 函数关闭资源, NonCancellable 非常有用.
下面是一个示例:
import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.milliseconds
//sampleStart
val serviceStarted = CompletableDeferred<Unit>()
fun startService() {
println("Starting the service...")
serviceStarted.complete(Unit)
}
suspend fun shutdownServiceAndWait() {
println("Shutting down...")
delay(100.milliseconds)
println("Successfully shut down!")
}
suspend fun main() {
withContext(Dispatchers.Default) {
val childJob = launch {
startService()
try {
awaitCancellation()
} finally {
withContext(NonCancellable) {
// 如果没有 withContext(NonCancellable),
// 这个函数不会完成, 因为协程已经被取消
shutdownServiceAndWait()
}
}
}
serviceStarted.await()
childJob.cancel()
}
println("Exiting the program")
}
//sampleEnd
超时
超时, 允许你在指定的时间之后自动取消协程. 对于停止耗时过长的操作, 超时很有用, 有助于让应用程序保持响应, 避免不必要的阻塞线程.
要指定超时, 请使用 withTimeoutOrNull() 函数, 指定 Duration 参数:
import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.milliseconds
//sampleStart
suspend fun slowOperation(): Int {
try {
delay(300.milliseconds)
return 5
} catch (e: CancellationException) {
println("The slow operation has been canceled: $e")
throw e
}
}
suspend fun fastOperation(): Int {
try {
delay(15.milliseconds)
return 14
} catch (e: CancellationException) {
println("The fast operation has been canceled: $e")
throw e
}
}
suspend fun main() {
withContext(Dispatchers.Default) {
val slow = withTimeoutOrNull(100.milliseconds) {
slowOperation()
}
println("The slow operation finished with $slow")
val fast = withTimeoutOrNull(100.milliseconds) {
fastOperation()
}
println("The fast operation finished with $fast")
}
}
//sampleEnd
如果运行时间超过了指定的 Duration, withTimeoutOrNull() 返回 null.
2026/08/12