> Как остановить корутину и гарантированно отменить ее в 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(), корутина получает сигнал, но не останавливается мгновенно. Механизм работает так:

  1. cancel() переводит Job в состояние Cancelling и бросает CancellationException в точке приостановки (suspension point).
  2. Если корутина в этот момент выполняет CPU-bound код без suspending-вызовов, она продолжит работу до ближайшей проверки отмены.
  3. Все suspending-функции из kotlinx.coroutines (delay, withContext, await, etc.) автоматически проверяют отмену и бросают CancellationException.
  4. 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 - невозможно отменить позже.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью