---
title: Отмена и таймауты
description: Кооперативная отмена, CancellationException, withTimeout, NonCancellable и освобождение ресурсов
tags:
- kotlin
- coroutines
---
Отмена корутин **кооперативная**: библиотека не может остановить код насильно, она лишь сообщает корутине, что её просят завершиться. Код должен это проверять - сам или через `suspend`-функции.
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
repeat(100) { i ->
delay(10)
println("шаг $i")
}
}
delay(35)
job.cancelAndJoin()
println("отменено")
// шаг 0
// шаг 1
// шаг 2
// отменено
}
```
Все `suspend`-функции из `kotlinx.coroutines` (`delay`, `join`, `await`, `withContext`) проверяют отмену и бросают `CancellationException`.
## Как работает отмена
`cancel()` переводит `Job` в состояние Cancelling и бросает `CancellationException` в точке приостановки:
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
try {
delay(1000)
} catch (e: CancellationException) {
println("поймали: ${e::class.simpleName}")
throw e // обязательно пробросить!
}
}
delay(20)
job.cancelAndJoin()
println("состояние: отменён=${job.isCancelled}, завершён=${job.isCompleted}")
// поймали: JobCancellationException
// состояние: отменён=true, завершён=true
}
```
`CancellationException` - особое исключение: оно не считается ошибкой и не приводит к падению родителя. Подробнее: [Исключения в корутинах](Исключения%20в%20корутинах.md)
## Код без точек приостановки не отменяется
```kotlin
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis
fun main() = runBlocking {
val t = measureTimeMillis {
val job = launch(Dispatchers.Default) {
var sum = 0L
repeat(200_000_000) { sum += it } // ни одной точки приостановки
println("досчитал несмотря на отмену: ${sum > 0}")
}
delay(20)
job.cancelAndJoin()
}
println("cancel ждал окончания цикла: ${t > 100}")
// досчитал несмотря на отмену: true
// cancel ждал окончания цикла: true
}
```
Два способа сделать такой код отменяемым:
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
// 1. проверять isActive
val job1 = launch(Dispatchers.Default) {
var i = 0
while (isActive && i < 200_000_000) i++
println("вышел по isActive: ${i < 200_000_000}")
}
delay(20)
job1.cancelAndJoin()
// 2. вызывать ensureActive() — бросит CancellationException
val job2 = launch(Dispatchers.Default) {
try {
repeat(200_000_000) {
if (it % 100_000 == 0) ensureActive()
}
} catch (e: CancellationException) {
println("вышел по ensureActive")
throw e
}
}
delay(20)
job2.cancelAndJoin()
// вышел по isActive: true
// вышел по ensureActive
}
```
Есть и третий вариант - `yield()`: он и проверяет отмену, и даёт другим корутинам шанс выполниться.
| Способ | Бросает исключение | Когда применять |
|---|---|---|
| `isActive` | нет, возвращает `Boolean` | условие цикла |
| `ensureActive()` | да, `CancellationException` | внутри тела, быстрый выход |
| `yield()` | да | ещё и уступает поток другим |
## Освобождение ресурсов
`finally` работает как обычно:
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
try {
println("ресурс открыт")
delay(1000)
} finally {
println("ресурс закрыт")
}
}
delay(20)
job.cancelAndJoin()
println("готово")
// ресурс открыт
// ресурс закрыт
// готово
}
```
Но внутри `finally` отменённой корутины **нельзя приостанавливаться**:
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
try {
delay(1000)
} finally {
val failed = runCatching { delay(50) }.isFailure
println("delay в finally упал: $failed") // delay в finally упал: true
}
}
delay(20)
job.cancelAndJoin()
}
```
Корутина уже отменена, поэтому любая приостановка немедленно бросает `CancellationException`. Если в очистке нужна приостановка - оберните её в `NonCancellable`:
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
try {
delay(1000)
} finally {
withContext(NonCancellable) {
delay(20)
println("асинхронная очистка выполнена")
}
}
}
delay(20)
job.cancelAndJoin()
println("готово")
// асинхронная очистка выполнена
// готово
}
```
`NonCancellable` нужен именно и только для этого. Использовать его, чтобы «защитить» обычную работу от отмены - ошибка: такая корутина станет неостановимой.
Для `Closeable`-ресурсов проще `use`:
```kotlin
import kotlinx.coroutines.*
class Connection : AutoCloseable {
fun query() = "результат"
override fun close() = println("соединение закрыто")
}
fun main() = runBlocking {
val job = launch {
Connection().use { conn ->
println(conn.query())
delay(1000)
}
}
delay(20)
job.cancelAndJoin()
// результат
// соединение закрыто
}
```
## withTimeout
```kotlin
import kotlinx.coroutines.*
suspend fun slowOperation(): String {
delay(1000)
return "готово"
}
fun main() = runBlocking {
try {
withTimeout(50) { slowOperation() }
} catch (e: TimeoutCancellationException) {
println("не успели: ${e.message}")
}
// не успели: Timed out waiting for 50 ms
}
```
`withTimeoutOrNull` вместо исключения возвращает `null` - обычно это удобнее:
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val fast = withTimeoutOrNull(100) { delay(20); "успели" }
val slow = withTimeoutOrNull(20) { delay(100); "не успели" }
println(fast) // успели
println(slow) // null
}
```
`TimeoutCancellationException` - подкласс `CancellationException`, поэтому он отменяет только свой блок, а не всю область.
## Таймаут и ресурсы
Опасное место: таймаут может сработать между получением ресурса и входом в `try`.
```kotlin
import kotlinx.coroutines.*
class Resource2 {
var closed = false
fun close() { closed = true }
}
fun main() = runBlocking {
var resource: Resource2? = null
withTimeoutOrNull(30) {
resource = Resource2() // создали
try {
delay(1000) // здесь таймаут
} finally {
resource?.close() // закрыли
}
}
println("ресурс закрыт: ${resource?.closed}") // ресурс закрыт: true
}
```
Правило: открывайте ресурс **внутри** блока и закрывайте в `finally` или через `use`. Открытие снаружи с закрытием внутри - источник утечек.
## Отмена распространяется вниз
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val parent = launch {
launch {
try { delay(1000) } finally { println("ребёнок 1 завершён") }
}
launch {
try { delay(1000) } finally { println("ребёнок 2 завершён") }
}
try { delay(1000) } finally { println("родитель завершён") }
}
delay(20)
parent.cancelAndJoin()
println("всё дерево остановлено")
// ребёнок 1 завершён
// ребёнок 2 завершён
// родитель завершён
// всё дерево остановлено
}
```
Это бесплатное следствие структурированной конкурентности: отменив одну корутину, вы останавливаете всё поддерево. Подробнее: [Структурированная конкурентность](Структурированная%20конкурентность.md)
```mermaid
sequenceDiagram
participant В as Вызывающий
participant Р as Родитель
participant Д as Дети
В->>Р: cancel()
Р->>Р: состояние Cancelling
Р->>Д: отменить всех детей
Д->>Д: CancellationException
в точке приостановки
Д->>Д: выполнить finally
Д-->>Р: ребёнок Cancelled
Р->>Р: выполнить finally
Р-->>В: состояние Cancelled, join() вернулся
```
## cancel против cancelAndJoin
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val job1 = launch { try { delay(1000) } finally { println("1: очистка") } }
job1.cancel() // не ждём завершения
println("после cancel: завершён=${job1.isCompleted}")
val job2 = launch { try { delay(1000) } finally { println("2: очистка") } }
job2.cancelAndJoin() // ждём
println("после cancelAndJoin: завершён=${job2.isCompleted}")
// после cancel: завершён=false
// 1: очистка
// 2: очистка
// после cancelAndJoin: завершён=true
}
```
`cancel()` только отправляет сигнал; `cancelAndJoin()` дожидается, пока корутина действительно остановится и выполнит очистку. Для корректного освобождения ресурсов нужен второй вариант.
## Отмена и async
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val deferred = async { delay(1000); "результат" }
delay(20)
deferred.cancel()
val error = runCatching { deferred.await() }.exceptionOrNull()
println("await после cancel бросил: ${error is CancellationException}") // await после cancel бросил: true
}
```
Отменённый `Deferred` при `await()` бросает `CancellationException`, а не возвращает `null`. Для «результат или ничего» используйте `withTimeoutOrNull`.
## Таймаут на весь scope
```kotlin
import kotlinx.coroutines.*
suspend fun loadWithDeadline(): List? = withTimeoutOrNull(60) {
coroutineScope {
val a = async { delay(20); "быстрый" }
val b = async { delay(1000); "медленный" }
listOf(a.await(), b.await())
}
}
fun main() = runBlocking {
println(loadWithDeadline()) // null — медленный не успел, отменились оба
}
```
Таймаут поверх `coroutineScope` даёт дедлайн на всю группу задач: не успел один - отменяются все. Это правильное поведение для запроса с общим SLA.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `Thread.interrupt()` | `job.cancel()` |
> | `Thread.isInterrupted()` | `isActive` |
> | `InterruptedException` | `CancellationException` |
> | `Future.cancel(true)` - одна задача | отмена всего поддерева |
> | `ExecutorService.shutdownNow()` | `scope.cancel()` |
> | `future.get(50, MILLISECONDS)` | `withTimeout(50)` |
> | `ScheduledExecutorService` для дедлайна | `withTimeout` |
> | флаг `volatile boolean stopped` | `isActive` |
>
> **Прерывание в Java тоже кооперативное, но работает хуже.** `Thread.interrupt()` ставит флаг и прерывает только блокирующие вызовы, которые умеют его замечать (`sleep`, `wait`, `join`, часть NIO). Проблемы знакомые: `InterruptedException` - проверяемое исключение, которое приходится ловить в каждом блокирующем вызове, а при перехвате легко потерять флаг:
> ```java
> try {
> Thread.sleep(1000);
> } catch (InterruptedException e) {
> // забыли Thread.currentThread().interrupt() — прерывание потеряно
> }
> ```
> В Kotlin `CancellationException` непроверяемое, поэтому его не нужно ловить везде, а `catch (e: Exception)` без проброса ломает отмену ровно так же - разница в том, что IDE и детекторы это подсказывают.
>
> **Отмена по дереву против отмены по задаче.** `Future.cancel(true)` прерывает один поток. Если задача породила подзадачи через другой `submit`, они продолжат работу, и отследить их можно только вручную - списком `Future`. В Kotlin `cancel()` на родителе гарантированно останавливает всё поддерево, включая задачи, запущенные тремя уровнями ниже.
>
> **Таймаут не требует отдельного планировщика.** В Java `future.get(50, MILLISECONDS)` прерывает **ожидание**, но не саму задачу - она продолжит выполняться в пуле. Чтобы остановить и её, нужен `cancel(true)` плюс поддержка прерывания в коде задачи. `withTimeout` отменяет сам блок:
> ```kotlin
> import kotlinx.coroutines.*
>
> fun main() = runBlocking {
> var finished = false
> withTimeoutOrNull(30) {
> try { delay(1000); finished = true } finally { println("задача остановлена") }
> }
> println("тело доработало: $finished")
> // задача остановлена
> // тело доработало: false
> }
> ```
>
> **`shutdownNow()` против `scope.cancel()`.** `shutdownNow` возвращает список незапущенных задач и прерывает потоки работающих - результат зависит от того, умеет ли код реагировать на прерывание. `scope.cancel()` отменяет корутины в известных точках приостановки, а `join()` даёт дождаться завершения очистки.
>
> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [Система исключений в Java](../../Программирование%20на%20Java/Исключения/Система%20исключений.md)
> [!warning] Подводные камни
> **Перехват `CancellationException` без проброса ломает отмену:**
> ```kotlin
> import kotlinx.coroutines.*
>
> fun main() = runBlocking {
> val job = launch {
> repeat(5) {
> try {
> delay(20)
> } catch (e: CancellationException) {
> println("проглотили отмену на шаге $it")
> // нет throw — цикл продолжается!
> }
> }
> println("корутина доработала до конца")
> }
> delay(30)
> job.cancelAndJoin()
> // проглотили отмену на шаге 1
> // проглотили отмену на шаге 2
> // проглотили отмену на шаге 3
> // проглотили отмену на шаге 4
> // корутина доработала до конца
> }
> ```
> Корутина стала неотменяемой. Та же беда возникает от `catch (e: Exception)` - `CancellationException` в него попадает.
>
> **`try/catch (Exception)` вокруг `suspend`-вызова перехватывает отмену:**
> ```kotlin
> import kotlinx.coroutines.*
>
> fun main() = runBlocking {
> val job = launch {
> try {
> delay(1000)
> } catch (e: Exception) {
> println("перехвачено как обычная ошибка: ${e is CancellationException}")
> }
> println("продолжили работу после отмены")
> }
> delay(20)
> job.cancelAndJoin()
> // перехвачено как обычная ошибка: true
> // продолжили работу после отмены
> }
> ```
> Правильно: сначала `catch (e: CancellationException) { throw e }`, затем `catch (e: Exception)`.
>
> **`runCatching` глотает `CancellationException`** по той же причине - он ловит `Throwable`. В корутинном коде это регулярная проблема: `runCatching { suspendCall() }` делает блок неотменяемым. Для `suspend`-кода нужен явный `try/catch` с пробросом отмены.
>
> **Приостановка в `finally` отменённой корутины не работает:**
> ```kotlin
> import kotlinx.coroutines.*
>
> fun main() = runBlocking {
> val job = launch {
> try {
> delay(1000)
> } finally {
> val ok = runCatching { delay(50); "очистка" }.isSuccess
> println("приостановка в finally удалась: $ok") // приостановка в finally удалась: false
> }
> }
> delay(20)
> job.cancelAndJoin()
> }
> ```
> Нужен `withContext(NonCancellable)`.
>
> **`NonCancellable` вокруг обычной работы делает корутину неостановимой:**
> ```kotlin
> import kotlinx.coroutines.*
> import kotlin.system.measureTimeMillis
>
> fun main() = runBlocking {
> val t = measureTimeMillis {
> val job = launch {
> withContext(NonCancellable) {
> delay(200) // отмена проигнорирована
> println("доработал несмотря на cancel")
> }
> }
> delay(20)
> job.cancelAndJoin()
> }
> println("cancel ждал 200 мс: ${t >= 190}")
> // доработал несмотря на cancel
> // cancel ждал 200 мс: true
> }
> ```
> `NonCancellable` оправдан только для короткой очистки в `finally`.
>
> **`cancel()` без `join()` не ждёт очистки.** Если после `cancel()` сразу закрыть ресурс или выйти из процесса, `finally` может не успеть выполниться. Используйте `cancelAndJoin()`.
>
> **Чистый вычислительный цикл игнорирует отмену.** Любой `while`/`repeat` без `suspend`-вызовов нужно дополнять `isActive` или `ensureActive()` - иначе корутина отработает до конца, а `cancelAndJoin()` будет её ждать.
>
> **`withTimeout` бросает исключение, которое легко не заметить:**
> ```kotlin
> import kotlinx.coroutines.*
>
> fun main() = runBlocking {
> val result = runCatching { withTimeout(20) { delay(100); "значение" } }
> println("упало с таймаутом: ${result.exceptionOrNull() is TimeoutCancellationException}")
> // упало с таймаутом: true
> }
> ```
> `TimeoutCancellationException` - подкласс `CancellationException`, поэтому обычный `catch (e: CancellationException) { throw e }` его пробросит, и таймаут «не сработает» там, где вы ждали обработки. Используйте `withTimeoutOrNull` или ловите `TimeoutCancellationException` до отмены.
>
> **Ресурс, открытый до `withTimeout`, может утечь:**
> ```kotlin
> import kotlinx.coroutines.*
>
> fun main() = runBlocking {
> var opened = 0
> var closed = 0
> withTimeoutOrNull(30) {
> opened++ // открыли внутри — хорошо
> try { delay(1000) } finally { closed++ }
> }
> println("открыто $opened, закрыто $closed") // открыто 1, закрыто 1
> }
> ```
> Если бы `opened++` стоял перед `withTimeoutOrNull`, а `closed++` внутри - при срабатывании таймаута до входа в блок ресурс остался бы открытым.
>
> **Отменённый scope нельзя переиспользовать** - `launch` в нём сразу возвращает отменённый `Job`. Для перезапуска создавайте новый scope.
>
> **`await()` на отменённом `Deferred` бросает исключение, а не возвращает `null`.** Код, ожидающий `null` при отмене, получит падение.
>
> **Отмена не мгновенна.** Между `cancel()` и фактическим завершением проходит время: корутина должна дойти до точки приостановки и выполнить `finally`. Проверка `isCompleted` сразу после `cancel()` обычно даёт `false`.
Далее: [Исключения в корутинах](Исключения%20в%20корутинах.md)