← Назад к вопросам

От чего зависит упадет ли корутина?

2.3 Middle🔥 143 комментариев
#Kotlin основы#Многопоточность и асинхронность

Комментарии (3)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

От чего зависит падение (failure) корутины?

Падение корутины в Kotlin зависит не от одной, а от комбинации факторов, связанных с механизмом обработки исключений и структурой иерархии корутин. Ключевой принцип: необработанное исключение в корутине приводит к её отмене (cancellation) и, в большинстве случаев, к распространению этого исключения вверх по иерархии, что может завершить всю цепочку связанных корутин или даже приложение.

1. Механизм обработки исключений и конструктор запуска (CoroutineStart / CoroutineScope)

Способ запуска корутины и используемый CoroutineScope напрямую определяют, как будут обрабатываться непойманные исключения.

a) launch (обычный строитель)

Используется для "fire-and-forget" задач. Необработанное исключение внутри launch приводит к:

  • Немедленной отмене самой корутины.
  • Распространению исключения вверх через Job родителя.
  • Если родитель — Job (внутри coroutineScope или supervisorScope), он также будет отменён.
  • Если родитель — SupervisorJob или используется supervisorScope, отмена будет изолирована.
// Пример: исключение в launch отменяет родительскую область (если не Supervisor)
fun example1() = runBlocking {
    val job = launch { // Родитель — Job из runBlocking
        delay(100)
        throw RuntimeException("Crash!")
    }
    job.join() // Исключение будет выброшено здесь и завершит runBlocking
}

b) async (строитель с результатом)

Исключение не выбрасывается немедленно. Оно "откладывается" и будет выброшено только при вызове .await() на возвращённом Deferred. Это ключевое отличие.

// Пример: исключение в async не падает, пока не вызван await
fun example2() = runBlocking {
    val deferred = async {
        throw RuntimeException("Hidden crash!")
    }
    delay(1000) // Корутина уже упала, но исключение молчит
    try {
        deferred.await() // Исключение выброшено ТОЛЬКО здесь!
    } catch (e: Exception) {
        println("Поймано: $e")
    }
}

2. Тип CoroutineScope и роль SupervisorJob

Это самый важный фактор, определяющий распространение отмены.

  • Обычная иерар thatрхия (например, coroutineScope { } или launch без Supervisor): Необработанное исключение в дочерней корутине приводит к каскадной отмене всех соседних дочерних корутин и самого родителя. Исключение всплывает до обработчика (см. пункт 3).

  • Иерархия с супервизором (SupervisorJob или supervisorScope): SupervisorJob изолирует отмену. Падение одной дочерней корутины:

    * Не отменяет другие дочерние корутины.
    * Не отменяет родительскую область.
    * Исключение всё равно будет передано обработчику контекста (если не перехвачено внутри).

// Пример: SupervisorScope изолирует падение
fun example3() = runBlocking {
    supervisorScope {
        val child1 = launch {
            delay(100)
            throw RuntimeException("Child 1 failed!")
        }
        val child2 = launch {
            repeat(5) {
                delay(200)
                println("Child 2 is alive") // Продолжит работать!
            }
        }
        child1.join() // Мы можем обработать исключение здесь
        child2.join()
    }
    println("Scope completed despite the failure") // Эта строка выполнится
}

3. Наличие CoroutineExceptionHandler в контексте

CoroutineExceptionHandler — это обработчик глобальных исключений для корутин, которые не могут быть выброшены другим способом (т.е., для корутин, запущенных через launch). Он не перехватывает исключения в async (они перехватываются в .await()), а также не работает внутри supervisorScope для непосредственных дочерних корутин? (Нет, работает, но есть нюансы). Он эффективно "логирует" или обрабатывает фатальные сбои.

Важно: CoroutineExceptionHandler должен быть установлен в контексте корневой корутины (например, в GlobalScope.launch или в собственном scope с SupervisorJob()). Для async он не сработает.

// Пример: использование CoroutineExceptionHandler
fun example4() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("Глобальный обработчик поймал: $exception")
    }

    val job = GlobalScope.launch(handler) { // Глобальная корневая корутина
        throw RuntimeException("Test exception")
    }
    job.join() // Без join приложение может завершиться раньше, чем сработает handler
}

4. Локализация перехвата исключения (try-catch)

Если исключение перехвачено внутри самой корутины с помощью try-catch, то оно не считается необработанным и не приводит к её падению в смысле отмены иерархии.

// Пример: локальный перехват исключения
fun example5() = runBlocking {
    val job = launch {
        try {
            throw IOException("Network error")
        } catch (e: IOException) {
            println("Ошибка обработана локально: $e")
            // Можно выполнить recovery-логику
        }
        println("Эта строка выполнится, корутина жива")
    }
    job.join() // Исключение НЕ будет выброшено здесь
}

Сводка в виде маркированного списка

  • Корутина "падает", если в ней возникает неперехваченное исключение.
  • Способ запуска (launch vs async) определяет момент выброса исключения наружу.
  • Тип родительского Job (Job vs SupervisorJob) определяет масштаб последствий:
    * `Job` → каскадная отмена.
    * `SupervisorJob` → изоляция сбоя.
  • Наличие CoroutineExceptionHandler определяет, можно ли глобально обработать исключение из корневых launch-корутин.
  • Локальный try-catch полностью предотвращает "падение" для внешнего наблюдателя.

Таким образом, упадёт ли корутина — это вопрос не бинарный, а структурный. Она всегда внутренне "падает" при исключении, но видимость и последствия этого сбоя для системы целиком зависят от перечисленных выше архитектурных решений. Правильное использование SupervisorScope и async/await с точечной обработкой — основа отказоустойчивых асинхронных систем на корутинах.

От чего зависит упадет ли корутина? | PrepBro