Как отменить корутину правильно?
Отмена корутины в Kotlin — это важная часть управления жизненным циклом асинхронных задач. В отличие от потоков, корутины поддерживают кооперативную отмену, что означает, что сама корутина должна периодически проверять, была ли она отменена, и корректно завершать свою работу. Это делает отмену безопасной, предсказуемой и контролируемой.
1. Как устроена отмена корутин
Каждая корутина имеет Job, который управляет её жизненным циклом. Вызов job.cancel() инициирует отмену:
val job = launch {
// выполняемая работа
}
job.cancel()
После вызова cancel() корутина помечается как отменённая, и при первой возможности должна завершиться. Отмена не означает мгновенное завершение, если внутри корутины нет suspend-вызовов, проверок или точки отмены.
2. Точки отмены: кооперативная модель
Корутины не "прерываются" принудительно. Чтобы отмена сработала, в теле корутины должны быть:
-
suspend-вызовы, такие как delay(), withContext(), channel.receive(), которые сами проверяют отмену.
-
или ручные проверки через ensureActive(), isActive.
Пример с delay():
val job = launch {
repeat(1000) {
delay(500L) // точка отмены
println("Working $it")
}
}
delay(2000L)
job.cancel()
Пример с ensureActive():
val job = launch {
for (i in 1..1000) {
ensureActive()
println("Processing $i")
}
}
3. Проверка отмены вручную
Если код не содержит suspend-вызовов, можно вручную проверять отмену:
val job = launch {
for (i in 1..1000) {
if (!isActive) break
heavyCalculation(i)
}
}
Здесь isActive — это свойство CoroutineScope, возвращающее false, если корутина была отменена.
4. Отмена с исключением CancellationException
Когда корутина отменяется, внутри неё генерируется CancellationException. Он не считается ошибкой — это механизм завершения.
val job = launch {
try {
delay(10000L)
} catch (e: CancellationException) {
println("Coroutine cancelled")
}
}
job.cancel()
Можно поймать это исключение, чтобы освободить ресурсы или логировать отмену.
5. Отклик на отмену в finally
Чтобы выполнить действия при отмене (например, очистка ресурсов, закрытие сокета), используют try/finally:
val job = launch {
try {
repeat(1000) {
println("Working $it")
delay(500L)
}
} finally {
println("Cleanup after cancellation")
}
}
Если в finally снова используются suspend-функции, нужно переключиться в NonCancellable контекст:
finally {
withContext(NonCancellable) {
delay(100) // даже при отмене сработает
println("Cleanup finished")
}
}
6. Отмена дочерних корутин — структурированная отмена
Отмена родительской корутины автоматически отменяет все дочерние. Это называется структурированной отменой.
val parentJob = CoroutineScope(Dispatchers.Default).launch {
launch {
delay(1000L)
println("Child 1")
}
launch {
delay(2000L)
println("Child 2")
}
}
delay(500L)
parentJob.cancel() // отменяются обе дочерние корутины
Если же дочерняя корутина запущена в GlobalScope, то она не будет отменена автоматически.
7. Отмена через timeout
Можно ограничить выполнение корутины по времени с помощью withTimeout или withTimeoutOrNull.
try {
withTimeout(2000L) {
repeat(10) {
println("Working $it")
delay(1000L)
}
}
} catch (e: TimeoutCancellationException) {
println("Timed out")
}
Или использовать withTimeoutOrNull — он не выбрасывает исключение:
val result = withTimeoutOrNull(2000L) {
longRunningTask()
}
if (result == null) println("Timeout")
8. Объединение cancel() с join()
Иногда полезно дождаться завершения отменённой корутины:
val job = launch {
try {
repeat(1000) {
delay(500L)
println("Working $it")
}
} finally {
println("Cancelled")
}
}
delay(2000L)
job.cancel()
job.join() // дождаться завершения
9. Отмена в Android через lifecycleScope и viewModelScope
В Android отмена происходит автоматически при уничтожении LifecycleOwner (например, Activity или Fragment):
lifecycleScope.launch {
// отменится при onDestroy()
}
Также в ViewModel:
viewModelScope.launch {
// отменится при очистке ViewModel
}
Это упрощает управление ресурсами и предотвращает утечки памяти.
10. Объединение нескольких Job
Можно комбинировать Job в один с помощью parent-child и отменять всё сразу:
val parent = CoroutineScope(Dispatchers.Default).launch {
val child1 = launch { ... }
val child2 = launch { ... }
val child3 = launch { ... }
}
parent.cancel() // отменит все дочерние задачи
Таким образом, отмена становится централизованной и управляемой.
Корректное использование отмены позволяет создавать надёжные, безопасные и контролируемые приложения, где каждая задача завершает свою работу корректно, не оставляя утечек ресурсов и не нарушая целостность выполнения.