> Как остановить корутину и гарантированно отменить ее в Kotlin (Kotlin, Android)
Уровень: senior · Роль: mobile · Категория: Технические вопросы
Компании: Астра
Стек: Kotlin, Android
> Пример ответа
Короткий ответ
Гарантированная отмена корутины в Kotlin достигается через механизм cooperative cancellation: вызов cancel() на Job переводит её в состояние Cancelling, но корутина завершится только при следующей проверке отмены - в suspending-функциях из kotlinx.coroutines (например, delay, withContext) или явном вызове ensureActive() / isActive. Для гарантии нужно, чтобы весь код внутри корутины был отменяемым: не использовать блокирующие операции без withContext(Dispatchers.IO) и не игнорировать CancellationException.
Подробное объяснение
Отмена в Kotlin Coroutines - кооперативная, а не принудительная. Когда вы вызываете job.cancel(), корутина получает сигнал, но не останавливается мгновенно. Механизм работает так:
cancel()переводитJobв состояниеCancellingи бросаетCancellationExceptionв точке приостановки (suspension point).- Если корутина в этот момент выполняет CPU-bound код без suspending-вызовов, она продолжит работу до ближайшей проверки отмены.
- Все suspending-функции из
kotlinx.coroutines(delay, withContext, await, etc.) автоматически проверяют отмену и бросаютCancellationException. CancellationException- это нормальное исключение для отмены: его нужно пробрасывать дальше, а не глотать. Если вы его перехватите и не пробросите, корутина не отменится.
Гарантированность обеспечивается тремя условиями:
- Корутина должна быть запущена в скоупе, который можно отменить (например,
CoroutineScopeвViewModelилиlifecycleScope). - Все под-корутины (children) отменяются автоматически при отмене родительской.
- Код внутри корутины должен быть отменяемым: либо использовать suspending-функции, либо явно проверять
isActive/ensureActive()в циклах и тяжёлых вычислениях.
Для блокирующих операций (например, чтение файла, работа с сокетом) нужно оборачивать их в withContext(Dispatchers.IO) - это делает их отменяемыми, так как withContext - suspending-функция.
На практике
В Android-разработке отмена корутин обычно привязана к жизненному циклу: viewModelScope отменяется автоматически при очистке ViewModel, lifecycleScope - при уничтожении LifecycleOwner. Но для ручного управления используйте Job и CoroutineScope.
Ключевые практики:
- Храните
JobилиCoroutineScopeи вызывайтеcancel()в нужный момент (например, вonCleared()илиonDestroyView()). - Для отмены всех дочерних корутин достаточно отменить родительскую
Job. - Используйте
withContext(NonCancellable)только для критических cleanup-операций, которые должны выполниться даже при отмене. - Не используйте
runBlockingдля отмены - это блокирующий вызов, он не отменяем. - Для параллельных задач используйте
coroutineScopeилиsupervisorScope- они автоматически отменяют все дочерние корутины при отмене родительской.
Пример кода
class MyViewModel : ViewModel() { private val viewModelJob = Job() private val viewModelScope = CoroutineScope(Dispatchers.Main + viewModelJob) fun startLongTask() { viewModelScope.launch { try { // Отменяемая suspending-операция val result = withContext(Dispatchers.IO) { // блокирующая операция, но отменяемая через withContext readLargeFile() } // CPU-bound цикл с проверкой отмены for (i in 0..1_000_000) { ensureActive() // бросает CancellationException, если отменено processItem(i) } } catch (e: CancellationException) { // Пробрасываем дальше - это нормальное поведение throw e } finally { // cleanup, выполнится даже при отмене if (isActive) { // только если корутина ещё активна } } } } override fun onCleared() { viewModelScope.cancel() // гарантированная отмена всех корутин в scope super.onCleared() } }
Как отвечать на собеседовании
Начните с ключевого термина - cooperative cancellation. Объясните, что cancel() не останавливает корутину мгновенно, а лишь сигнализирует. Затем опишите механизм: CancellationException, проверки в suspending-функциях, ensureActive().
Подчеркните разницу между отменой Job и отменой CoroutineScope: скоуп отменяет все дочерние корутины. Упомяните, что withContext(NonCancellable) - это исключение для cleanup, но злоупотреблять им нельзя.
Если спросят про гарантии - скажите, что гарантия достигается только если код внутри корутины написан с учётом отменяемости. Приведите пример с блокирующим кодом без withContext(Dispatchers.IO) - такая корутина не отменится, пока блокирующий код не завершится.
Что проверяет интервьюер
- Понимание кооперативной модели отмены, а не принудительной.
- Умение объяснить, почему
cancel()не останавливает корутину мгновенно. - Знание
CancellationExceptionи правил его обработки (пробрасывать, не глотать). - Понимание разницы между
Job.cancel()иCoroutineScope.cancel(). - Умение применять
ensureActive()/isActiveв циклах и CPU-bound коде. - Знание
withContext(Dispatchers.IO)для отменяемости блокирующих операций. - Понимание жизненного цикла корутин в Android (viewModelScope, lifecycleScope).
Типичные ошибки
- Перехват
CancellationExceptionбез проброса - корутина не отменится, и это приведёт к утечке или зависанию. - Использование
runBlockingвнутри корутины - блокирует поток и не отменяется. - Игнорирование проверки
isActiveв длительных циклах - корутина продолжит работу послеcancel(). - Вызов
cancel()на дочернейJobвместо родительской - дочерние корутины не отменятся. - Использование
withContext(NonCancellable)для обычных операций, а не только для cleanup. - Запуск корутин в глобальном скоупе (
GlobalScope) - нет гарантии отмены. - Неявное создание корутин без сохранения
Job- невозможно отменить позже.
> Похожие задачи по mobile
Каким ключевым словом обозначаются параметры конструктора в data class Kotlin, если они используются только в init блоке
Как работают final и static в Kotlin и Java
Что такое smart cast в Kotlin
Может ли suspend функция работать вне CoroutineScope
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью