Отмена корутин и тайм-ауты
Отмена позволяет запросить остановку корутины до её завершения. Она останавливает работу, которая больше не нужна, например, когда пользователь закрывает окно или переходит на другую страницу интерфейса, пока корутина ещё выполняется.
Отмену можно использовать, чтобы раньше освободить ресурсы и предотвратить обращение корутины к объектам после их удаления. Также с её помощью можно останавливать долго работающие корутины, выполняющие повторяющиеся задачи, например:
Отправка сигналов проверки связи.
Выполнение запланированных задач.
Обновление состояния с учётом последнего показания, например в интерфейсе часов.
Отмена работает через дескриптор Job, который представляет жизненный цикл корутины и её связи с родительскими и дочерними корутинами. Job позволяет проверить, активна ли корутина, и отменить её вместе с дочерними корутинами в соответствии с принципом структурированной конкурентности.
Отмена корутин
Корутина отменяется при вызове функции cancel() для её дескриптора Job. Функции-конструкторы корутин, такие как .launch(), возвращают Job. Функция .async() возвращает Deferred, который реализует Job и поддерживает такое же поведение при отмене.
Функцию cancel() можно вызвать вручную, либо она может быть вызвана автоматически в результате распространения отмены при отмене родительской корутины.
При отмене корутина выбрасывает исключение CancellationException при следующей проверке отмены. Приостанавливающие функции библиотеки kotlinx.coroutines, например функция delay(), проверяют наличие отмены при приостановке.
Функцию awaitCancellation() можно использовать, чтобы приостановить корутину до её отмены. Она эквивалентна вызову delay(Duration.INFINITE).
Ниже показано, как отменить корутину вручную:
import kotlinx.coroutines.*
suspend fun main() {
//sampleStart
withContext(Dispatchers.Default) {
// Used as a signal that the coroutine has started running
val childStarted = CompletableDeferred<Unit>()
val childJob: Job = launch {
println("The coroutine has started")
// Completes the CompletableDeferred,
// signaling that the coroutine has started running
childStarted.complete(Unit)
try {
// Suspends indefinitely
// This call will never return unless the coroutine is canceled
awaitCancellation()
} catch (e: CancellationException) {
println("The coroutine was canceled: $e")
// Always rethrow cancellation exceptions!
throw e
}
println("This line will never be executed")
}
// Waits for the coroutine to start before canceling it
childStarted.await()
// Cancels the coroutine,
// so awaitCancellation() throws a CancellationException
childJob.cancel()
}
// Coroutine builders such as withContext() or coroutineScope()
// wait for all child coroutines to complete,
// even when the children are canceled
println("All coroutines have completed")
//sampleEnd
}
В этом примере CompletableDeferred используется как сигнал о том, что корутина начала выполняться. При запуске корутина вызывает complete(), а await() возвращает управление только после завершения этого CompletableDeferred. Для отмены корутины такая проверка не нужна. Она добавлена, чтобы пример можно было воспроизвести: благодаря ей корутина успевает запуститься и вывести сообщения до отмены.
Поскольку Deferred реализует Job, отмена работает так же и для корутин, созданных функцией-конструктором корутины async():
val deferred = async { /* ... */ }
deferred.cancel()
Распространение отмены
Структурированная конкурентность гарантирует, что при отмене корутины отменяются и все её дочерние корутины. Это не позволяет дочерним корутинам продолжать работу после отмены родительской.
Вот пример:
import kotlinx.coroutines.*
suspend fun main() {
withContext(Dispatchers.Default) {
//sampleStart
// Used as a signal that the child coroutines have been launched
val childrenLaunched = CompletableDeferred<Unit>()
// Launches two child coroutines
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")
}
}
// Completes the CompletableDeferred,
// signaling that the child coroutines have been launched
childrenLaunched.complete(Unit)
}
// Waits for the parent coroutine to signal that it has launched
// all of its children
childrenLaunched.await()
// Cancels the parent coroutine, which cancels all its children
parentJob.cancel()
//sampleEnd
}
}
В этом примере каждая дочерняя корутина использует блок finally, поэтому содержащийся в нём код выполняется при отмене корутины. Здесь CompletableDeferred сигнализирует о том, что дочерние корутины запущены, но не гарантирует, что они начали выполняться. Если их отменить раньше, ничего не будет выведено.
Реакция корутин на отмену
В Kotlin отмена корутин кооперативна. Корутины реагируют на отмену, только если содействуют ей, приостанавливаясь или явно проверяя наличие отмены.
В этом разделе вы узнаете, как добавление точек приостановки, например вызовов функции yield(), позволяет корутинам реагировать на отмену.
Точки приостановки и отмена
После отмены корутина продолжает выполняться, пока не достигнет точки в коде, в которой она может приостановиться, — так называемой точки приостановки. При приостановке приостанавливающая функция проверяет, была ли корутина отменена. Если да, корутина останавливается и выбрасывает 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() {
//sampleStart
withContext(Dispatchers.Default) {
val childJobs = listOf(
launch {
// Suspends until canceled
awaitCancellation()
},
launch {
// Suspends until canceled
delay(Duration.INFINITE)
},
launch {
val channel = Channel<Int>()
// Suspends while waiting for a value that's never sent
channel.receive()
},
launch {
val deferred = CompletableDeferred<Int>()
// Suspends while waiting for a value that's never completed
deferred.await()
},
launch {
val mutex = Mutex(locked = true)
// Suspends while waiting for a mutex that remains locked indefinitely
mutex.lock()
}
)
// Gives the child coroutines time to start and suspend
delay(100.milliseconds)
// Cancels all child coroutines
childJobs.forEach { it.cancel() }
}
println("All child jobs completed!")
//sampleEnd
}
Приостанавливающая функция yield()
Если корутина не приостанавливается, другие корутины не могут выполняться в том же потоке до её завершения. Поэтому корутины, которые не приостанавливаются, выполняются в этом потоке последовательно. Если корутина долго не приостанавливается, она также не остановится при отмене.
В вычислениях с интенсивной нагрузкой на процессор и другом коде, который долго выполняется без приостановки, периодически вызывайте функцию yield(). Эта функция освобождает текущий поток, позволяя другим корутинам выполняться в нём. Она также гарантирует, что корутина будет регулярно проверять наличие отмены. Если корутина отменена, функция yield() выбрасывает CancellationException.
Вот пример:
import kotlinx.coroutines.*
fun main() {
//sampleStart
// runBlocking uses the current thread for running all coroutines
runBlocking {
val coroutineCount = 5
repeat(coroutineCount) { coroutineIndex ->
launch {
val id = coroutineIndex + 1
repeat(5) { iterationIndex ->
val iteration = iterationIndex + 1
// Suspends temporarily to give other coroutines a chance to run
// Without this, the coroutines run sequentially
yield()
// Prints the coroutine index and iteration index
println("$id * $iteration = ${id * iteration}")
}
}
}
}
//sampleEnd
}
В этом примере каждая корутина использует yield(), чтобы дать другим корутинам возможность выполняться между итерациями.
Явная проверка наличия отмены
Проверку наличия отмены можно выполнять явно: это позволяет долго работающему коду реагировать на отмену без приостановки. Долго работающая корутина, которая не приостанавливается, может мешать другим корутинам в том же потоке выполняться до своего завершения. Если такое поведение не требуется, используйте вместо этого функцию yield().
В зависимости от API проверка либо возвращает логическое значение, либо выбрасывает исключение:
Свойство
isActiveвозвращаетfalse, если корутина отменена.Функция
ensureActive()выбрасываетCancellationException, если корутина отменена.
Прерывание блокирующего кода при отмене корутин
В JVM некоторые блокирующие функции, такие как Thread.sleep() или BlockingQueue.take(), могут блокировать текущий поток. Такие блокирующие функции можно прервать, чтобы досрочно остановить их выполнение. Однако при вызове из корутины её отмена не прерывает поток.
Чтобы прервать поток при отмене корутины, оберните блокирующий код в функцию runInterruptible():
import kotlinx.coroutines.*
suspend fun main() {
//sampleStart
withContext(Dispatchers.Default) {
val childStarted = CompletableDeferred<Unit>()
val childJob = launch {
try {
// Cancellation triggers a thread interruption
runInterruptible {
childStarted.complete(Unit)
try {
// Blocks the current thread for a very long time
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()
// Cancels the coroutine and interrupts the thread executing Thread.sleep()
childJob.cancel()
}
//sampleEnd
}
Безопасная обработка значений при отмене корутин
При отмене приостановленная корутина возобновляется с исключением CancellationException, а не возвращает значение, даже если оно уже доступно. Такое поведение называется немедленной отменой. Оно не позволяет коду продолжать выполнение в области видимости отменённой корутины, например обновлять уже закрытый экран.
Вот пример:
// Defines a coroutine scope that uses the UI thread
class ScreenWithButtons(private val scope: CoroutineScope) {
fun loadAndUpdateButtons(filename: String) {
scope.launch {
// withContext() checks for cancellation before entering the block
// and after the block returns
val buttonNames = withContext(Dispatchers.IO) {
// This is a blocking call that doesn't react to cancellation
readLines(filename)
}
// It's safe to call updateUi()
// because withContext() doesn't return if the coroutine is canceled,
// and no code running on the UI thread can dispose of the buttons before this call
updateUi(buttonNames)
}
}
// Call this function only from the UI thread because it accesses the buttons
// Throws an exception if called after the buttons are disposed
private fun updateUi(buttonNames: List<String>) {
// Placeholder code that updates the buttons with specified names
}
// Call this function only from the UI thread
fun leaveScreen() {
// Cancels the scope when leaving the screen
// You can no longer update the UI
scope.cancel()
}
}
// UI controller code
setHandler(Event.ScreenClosed) {
// Runs on the UI thread
screenWithButtons.leaveScreen()
buttons.dispose()
}
В этом примере withContext(Dispatchers.IO) поддерживает отмену и не позволяет выполнить updateUi(), если функция leaveScreen() отменяет корутину до того, как withContext(Dispatchers.IO) вернёт имена кнопок.
Хотя немедленная отмена не позволяет использовать значения после того, как они утратили актуальность, она также может остановить код, пока важное значение ещё используется, что может привести к его потере. Такое может произойти, если корутина получает значение, например ресурс AutoCloseable, но отменяется до того, как доходит до кода, закрывающего этот ресурс. Чтобы этого избежать, размещайте код очистки в месте, выполнение которого гарантировано даже при отмене корутины, получившей значение.
Вот пример:
import java.nio.file.*
import java.nio.charset.*
import kotlinx.coroutines.*
import java.io.*
// Uses a scope that runs its coroutines on the UI thread
class ScreenWithFileContents(private val scope: CoroutineScope) {
fun displayFile(path: Path) {
scope.launch {
// Stores the reader in a variable, so the finally block can close it
var reader: BufferedReader? = null
try {
withContext(Dispatchers.IO) {
reader = Files.newBufferedReader(
path, Charset.forName("US-ASCII")
)
}
// Uses the stored reader after withContext() completes
updateUi(reader!!)
} finally {
// Ensures the reader is closed even when the coroutine is canceled
reader?.close()
}
}
}
private suspend fun updateUi(reader: BufferedReader) {
// Shows the file contents
while (true) {
val line = withContext(Dispatchers.IO) {
reader.readLine()
}
if (line == null)
break
addOneLineToUi(line)
}
}
private fun addOneLineToUi(line: String) {
// Placeholder for code that adds one line to the UI
}
// Only callable from the UI thread
fun leaveScreen() {
// Cancels the scope and prevents its coroutines from updating the UI
scope.cancel()
}
}
В этом примере сохранение BufferedReader в переменной и закрытие ресурса в блоке finally гарантируют его освобождение даже при отмене корутины.
Выполнение блоков, не подверженных отмене
Отмену можно сделать не влияющей на определённые участки корутины. Для этого передайте NonCancellable в качестве аргумента функции-конструктора корутины withContext().
NonCancellable полезен, когда нужно гарантировать завершение некоторых операций, например закрытия ресурсов с помощью приостанавливающей функции close(), даже если корутина отменена до их завершения.
Вот пример:
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) {
// Without withContext(NonCancellable),
// this function doesn't complete because the coroutine is canceled
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(): String {
try {
delay(300.milliseconds)
return "A"
} catch (e: CancellationException) {
println("The slow operation has been canceled: $e")
throw e
}
}
suspend fun fastOperation(): String {
try {
delay(15.milliseconds)
return "B"
} 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.
© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/coroutines-cancellation.html