Статья о том, как в Kotlin устроено завершение корутин.
РЕКОМЕНДУЕМ:
- Четыре ошибки при использовании корутин и Flow
- Как работают корутины в Kotlin
- Что такое CoroutineContext Kotlin
Начнем с того, что интерфейс Job, представляющий корутину, имеет метод cancel(), предназначенный для завершения корутины. Его вызов приводит к следующему:
- Корутина завершается на первой точке остановки, то есть когда происходит вызов какой‑то стандартной suspend-функции из стандартной библиотеки корутин (в случае с примером ниже эта точка — метод delay()).
- Если Job имеет потомков, то они тоже будут завершены.
- Job будет завершена, она больше не может быть использована для запуска новых корутин (состояние Cancelled).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
fun main() = runBlocking { val job = launch { repeat(1_000) { i -> delay(200) println("Printing $i") } } delay(1100) job.cancel() job.join() println("Cancelled successfully") } // Printing 0 // Printing 1 // Printing 2 // Printing 3 // Printing 4 // Cancelled successfully |
Как и в этом примере, зачастую после job.cancel() следует использовать job.join(), чтобы дождаться фактического завершения. Это настолько частая потребность, что библиотека поддержки корутин включает в себя функцию‑расширение cancelAndJoin().
Когда Job получает сигнал завершения, она меняет свое состояние на Cancelling. Затем, при переходе в следующую точку остановки, она выбрасывает исключение CancellationException. Это исключение можно перехватить, но, чтобы избежать трудноуловимых багов, его лучше сразу выбросить снова:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
suspend fun main(): Unit = coroutineScope { val job = Job() launch(job) { try { repeat(1_000) { i -> delay(200) println("Printing $i") } } catch (e: CancellationException) { println(e) throw e } } delay(1100) job.cancelAndJoin() println("Cancelled successfully") delay(1000) } // Printing 0 // Printing 1 // Printing 2 // Printing 3 // Printing 4 // JobCancellationException... // Cancelled successfully |
Благодаря тому что завершение корутины приводит к выбросу исключения, мы можем использовать блок finally, чтобы корректно закрыть все используемые корутиной ресурсы (базы данных, файлы, сетевые соединения). Однако мы уже не сможем в этом блоке запустить другие корутины или вызывать suspend-функции. Эти действия будут запрещены после перехода корутины в состояние Cancelling. Единственный выход из этой ситуации — использовать блок withContext(NonCancellable), который позволит вызвать suspend-функции, но не будет реагировать на сигнал завершения:
1 2 3 4 5 6 7 8 9 10 11 12 |
launch(job) { try { delay(200) println("Coroutine finished") } finally { println("Finally") withContext(NonCancellable) { delay(1000L) println("Cleanup done") } } } |
Еще один способ выполнить код после завершения корутины — это метод invokeOnCompletion(), который будет вызван независимо от того, как была завершена корутина — принудительно или она просто отработала свое время.
1 2 3 4 5 6 7 8 9 10 11 |
suspend fun main(): Unit = coroutineScope { val job = launch { delay(1000) } job.invokeOnCompletion { exception: Throwable? -> println("Finished") } delay(400) job.cancelAndJoin() } // Finished |
Завершение корутины происходит в точках остановки. Но что делать, если в коде корутины нет точек остановки (нет вызовов suspend-функций)?
Один из вариантов — использовать функцию yield(). Эта suspend-функция приостановит корутину и тут же ее возобновит, но по пути обработает сигнал завершения. Второй вариант: Boolean-поле isActive, достаточно просто проверить его значение и, если оно равно false, закончить работу внутри корутины. Еще один вариант — вызвать функцию ensureActive(). Она выбросит исключение CancellationException, если корутина уже получила сигнал завершения.
Cancellation in Kotlin Coroutines