Отмена и сбои корутин
Как останавливается дерево корутин и как распространяется сбой — иерархия Job, кооперативная отмена и yield, SupervisorJob и supervisorScope, где действительно срабатывает CoroutineExceptionHandler, и безопасная при отмене очистка через finally и NonCancellable.
9 вопросов
JuniorТеорияОчень частоЧто происходит с детьми корутины, когда отменяют её Job?
Что происходит с детьми корутины, когда отменяют её Job?
Отмена Job отменяет всё его поддерево — каждый ребёнок тоже отменяется, а родитель не считается завершённым, пока они все не закончат. Job — это ручка жизненного цикла, а не результат: он сообщает, активна работа, отменена или завершена, и не несёт значения.
Типичные ошибки
- ✗Думать, что корутина-ребёнок переживает отмену родителя
- ✗Считать
Jobхранилищем результата, а не ручкой жизненного цикла - ✗Полагать, что отменённый родитель завершён сразу, ещё до остановки детей
Уточняющие вопросы
- →Что даёт
cancelAndJoin()сверх обычногоcancel()? - →Чем
Deferredотличается здесь от обычногоJob?
JuniorТеорияЧастоЧто делает yield() внутри корутины и когда он нужен?
Что делает yield() внутри корутины и когда он нужен?
yield() — это точка приостановки: он даёт диспетчеру шанс выполнить другие корутины, а при возобновлении бросает CancellationException, если корутину отменили. Длинному циклу без приостановок он необходим — иначе тот вообще не заметит отмены.
Типичные ошибки
- ✗Считать, что отменённую корутину останавливают принудительно в любом месте
- ✗Думать, что
yield()блокирует поток, а не приостанавливает корутину - ✗Полагать, что цикл сам проверяет отмену, без всякой точки приостановки
Уточняющие вопросы
- →Чем
ensureActive()отличается отyield()в плотном цикле? - →Что показывает
isActiveсразу после вызоваcancel()?
MiddleДебаггингЧастоПочему этот CoroutineExceptionHandler ни разу не вызывается при сбое?
Почему этот CoroutineExceptionHandler ни разу не вызывается при сбое?
Обработчик срабатывает только на корутине, которая обрабатывает сбой, — на корне scope. На ребёнке он игнорируется: исключение уходит наверх, родителю. Ставьте его в контекст scope. async его не зовёт вовсе — исключение перебрасывает await().
Типичные ошибки
- ✗Ставить обработчик на ребёнка вместо корня scope
- ✗Ждать срабатывания обработчика на сбое внутри
async - ✗Думать, что
try/catchвокругlaunchпоймает брошенное ребёнком
Уточняющие вопросы
- →Что меняет
SupervisorJobв том, какая корутина обрабатывает сбой? - →Куда уходит исключение, если обработчик не установлен вовсе?
MiddleТеорияЧастоЧем coroutineScope и supervisorScope различаются, когда падает один ребёнок?
Чем coroutineScope и supervisorScope различаются, когда падает один ребёнок?
Оба приостанавливаются, пока не закончат все дети. Под coroutineScope упавший ребёнок отменяет братьев, а блок перебрасывает сбой. Под supervisorScope сбой остаётся при ребёнке: братья работают дальше, а об ошибке сообщает обработчик.
Типичные ошибки
- ✗Думать, что
coroutineScopeне дожидается своих детей - ✗Ждать отмены брата при сбое под
supervisorScope - ✗Считать, что
supervisorScopeполностью проглатывает сбой ребёнка
Уточняющие вопросы
- →Какой из двух подходит для нескольких независимых секций одного экрана?
- →Что произойдёт, если бросит само тело
supervisorScope?
MiddleТеорияЧастоКак SupervisorJob меняет распространение сбоя ребёнка?
Как SupervisorJob меняет распространение сбоя ребёнка?
С обычным Job упавший ребёнок отменяет родителя, а тот — всех остальных детей. SupervisorJob пускает сбой только вниз: упавший ребёнок гибнет один, братья работают дальше. Отмена родителя по-прежнему отменяет всех детей.
Типичные ошибки
- ✗Думать, что
SupervisorJobпроглатывает сбой, а не изолирует его - ✗Ждать, что при падении одного ребёнка под супервизором погибнут и братья
- ✗Считать, что отмена супервизора оставляет его детей работать
Уточняющие вопросы
- →Где должен быть установлен
SupervisorJob, чтобы такая изоляция работала? - →Кто тогда сообщает о сбое ребёнка под
SupervisorJob?
MiddleДебаггингИногдаПочему этот фильтр продолжает жечь CPU после закрытия экрана?
Почему этот фильтр продолжает жечь CPU после закрытия экрана?
Отмена кооперативна: она срабатывает лишь в точке приостановки или на явной проверке, а в цикле нет ни того, ни другого — поэтому он доходит до конца. Проверяйте каждую итерацию через ensureActive() или yield() либо оберните цикл в while (isActive).
Типичные ошибки
- ✗Считать, что отменённую корутину останавливают принудительно в произвольном месте
- ✗Думать, что отмену «доставляет» диспетчер, а не точка приостановки
- ✗Полагать, что CPU-цикл сам прерывается между итерациями
Уточняющие вопросы
- →Чем
ensureActive()отличается здесь от чтенияisActive? - →Что даст здесь
yield()сверх проверки на отмену?
MiddleДизайнИногдаРепозиторий открывает временный файл и транзакцию базы внутри корутины и закрывает оба в блоке finally. Оба вызова close — suspend-функции. Когда пользователь уходит с экрана, scope отменяется — и оказывается, что блок finally выполняется, а вот два приостанавливающих close не выполняются никогда: временные файлы копятся, транзакция остаётся открытой. Объясните, как отмена взаимодействует с блоком finally в корутине, почему приостанавливающий вызов внутри него падает и как вы напишете эту очистку, чтобы она всегда доходила до конца. Назовите ограничения, которые вы наложите на предложенную очистку.
Репозиторий открывает временный файл и транзакцию базы внутри корутины и закрывает оба в блоке finally. Оба вызова close — suspend-функции. Когда пользователь уходит с экрана, scope отменяется — и оказывается, что блок finally выполняется, а вот два приостанавливающих close не выполняются никогда: временные файлы копятся, транзакция остаётся открытой. Объясните, как отмена взаимодействует с блоком finally в корутине, почему приостанавливающий вызов внутри него падает и как вы напишете эту очистку, чтобы она всегда доходила до конца. Назовите ограничения, которые вы наложите на предложенную очистку.
У отменённой корутины Job уже неактивен, поэтому любой приостанавливающий вызов в finally сразу падает с CancellationException, и очистка не выполняется. Оберните её в withContext(NonCancellable) — и держите короткой и ограниченной, ведь остановить её нечем.
Типичные ошибки
- ✗Думать, что при отмене
finallyпропускается целиком - ✗Ждать, что приостанавливающий вызов отработает как обычно в отменённой корутине
- ✗Помещать длинную или неограниченную работу внутрь
NonCancellable
Уточняющие вопросы
- →Почему участок
NonCancellableдолжен быть коротким и ограниченным? - →Какой очистке
NonCancellableне нужен вовсе?
SeniorДизайнИногдаViewModel дашборда грузит пять независимых виджетов — профиль, ленту, уведомления, биллинг, промо — каждый своим приостанавливающим вызовом репозитория, все запущены в viewModelScope. Сейчас один нестабильный виджет кладёт весь экран: его сбой отменяет остальные четыре, и пользователь видит пустую страницу с ошибкой. Продукт хочет, чтобы каждый виджет падал сам по себе, показывая ошибку внутри своей карточки, а остальной дашборд оставался рабочим — и чтобы пользователь по-прежнему мог закрыть экран и отменить все пять разом. Опишите scope и обработку сбоев, которые вы построите, где именно наблюдается каждый сбой и почему try/catch вокруг launch эту задачу не решает.
ViewModel дашборда грузит пять независимых виджетов — профиль, ленту, уведомления, биллинг, промо — каждый своим приостанавливающим вызовом репозитория, все запущены в viewModelScope. Сейчас один нестабильный виджет кладёт весь экран: его сбой отменяет остальные четыре, и пользователь видит пустую страницу с ошибкой. Продукт хочет, чтобы каждый виджет падал сам по себе, показывая ошибку внутри своей карточки, а остальной дашборд оставался рабочим — и чтобы пользователь по-прежнему мог закрыть экран и отменить все пять разом. Опишите scope и обработку сбоев, которые вы построите, где именно наблюдается каждый сбой и почему try/catch вокруг launch эту задачу не решает.
Дайте виджетам супервизор — supervisorScope или scope с SupervisorJob, — чтобы сбой шёл только вниз и не отменял братьев. Ловите ошибку внутри каждого ребёнка и кладите её в состояние карточки: try вокруг launch не ловит ничего, ведь ребёнок падает позже.
Типичные ошибки
- ✗Ждать, что
tryвокругlaunchпоймает брошенное ребёнком - ✗Вешать
CoroutineExceptionHandlerна ребёнка и ждать его срабатывания - ✗Брать
GlobalScopeради изоляции сбоев, теряя вместе с ним отмену
Уточняющие вопросы
- →Что под таким супервизором по-прежнему отменяет все пять виджетов разом?
- →Куда вы поместите повтор для одного упавшего виджета?
SeniorДебаггингИногдаПочему уход с экрана логирует сетевую ошибку и оставляет временный файл?
Почему уход с экрана логирует сетевую ошибку и оставляет временный файл?
Отмена приходит как CancellationException, а catch (e: Exception) её проглатывает — обычная отмена логируется как сетевой сбой. Приостанавливающий deleteTemp() в finally затем падает сразу, ведь Job уже отменён. Перебросьте отмену; очистку — в NonCancellable.
Типичные ошибки
- ✗Ловить
Exception(или братьrunCatching) вокруг приостанавливающего вызова и глотать отмену - ✗Сообщать о
CancellationExceptionпользователю как о настоящем сбое - ✗Ждать, что приостанавливающая очистка в
finallyотработает в отменённой корутине
Уточняющие вопросы
- →Почему
runCatchingздесь опаснее, чемcatch (e: Exception)? - →Какие сбои этот
catchзагрузки всё же должен обрабатывать сам?