Как остановить корутину и гарантированно отменить ее в Kotlin
Что проверяют
Понимаете ли вы, что отмена корутин кооперативная и cancel() сам ничего не останавливает. Следом спрашивают, почему catch (e: Exception) ломает отмену и как выполнить suspend-вызов в finally после отмены.
Как отвечать
Механика: job.cancel() переводит Job в состояние отмены, а сама корутина останавливается в ближайшей точке приостановки, которая проверяет отмену. Такие точки это delay, withContext, yield и почти все suspend-функции kotlinx.coroutines: они бросают CancellationException, и корутина завершается. Отсюда правила:
- вычислительный цикл без suspend-вызовов не остановится никогда. Внутри нужны проверки
ensureActive(),isActiveилиyield(); - блокирующий код (
Thread.sleep, блокирующий сокет, старый JDBC) отмену не видит. Оборачивайте его вrunInterruptible(Dispatchers.IO) { ... }, тогда отмена превращается в прерывание потока; - дождаться завершения:
job.cancelAndJoin(), иначе код послеcancel()выполнится, пока корутина ещё работает; - очистка в
finally: любой suspend-вызов в отменённой корутине снова броситCancellationException, поэтому закрытие ресурсов с приостановкой делают вwithContext(NonCancellable) { ... }; - структурированная конкурентность: отмена родителя отменяет всех детей. На Android корутины запускают в
viewModelScope(отменяется вonCleared) иlifecycleScope, а сбор Flow оборачивают вrepeatOnLifecycle(Lifecycle.State.STARTED).
Голый CoroutineScope(Dispatchers.Main).launch или GlobalScope никто не отменит, это прямая дорога к утечке экрана.
Где спотыкаются
Ловят исключения широко: catch (e: Exception) или runCatching глотают CancellationException, и корутина продолжает работу после отмены. Его нужно пробрасывать дальше или ловить конкретные типы.
Считают, что cancel() прерывает поток, как Thread.interrupt(). Цикл с тяжёлой математикой без проверок спокойно досчитает до конца.
Вызывают suspend-функцию в finally без NonCancellable. Запись в базу или отправка события при закрытии экрана молча не выполняется.