От чего зависит упадет ли корутина?
Комментарии (3)
Ответ сгенерирован нейросетью и может содержать ошибки
От чего зависит падение (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() // Исключение НЕ будет выброшено здесь
}
Сводка в виде маркированного списка
- Корутина "падает", если в ней возникает неперехваченное исключение.
- Способ запуска (
launchvsasync) определяет момент выброса исключения наружу. - Тип родительского Job (
JobvsSupervisorJob) определяет масштаб последствий:
* `Job` → каскадная отмена.
* `SupervisorJob` → изоляция сбоя.
- Наличие
CoroutineExceptionHandlerопределяет, можно ли глобально обработать исключение из корневыхlaunch-корутин. - Локальный
try-catchполностью предотвращает "падение" для внешнего наблюдателя.
Таким образом, упадёт ли корутина — это вопрос не бинарный, а структурный. Она всегда внутренне "падает" при исключении, но видимость и последствия этого сбоя для системы целиком зависят от перечисленных выше архитектурных решений. Правильное использование SupervisorScope и async/await с точечной обработкой — основа отказоустойчивых асинхронных систем на корутинах.