Cancellation in Kotlin Coroutines - большая статья о том, как в Kotlin устроено завершение корутин.
Итак, начнем с того, что интерфейс Job, представляющий корутину, имеет метод cancel(), предназначенный для завершения корутины. Его вызов приводит к следующему:
* Корутина завершается на первой точке остановки, то есть когда происходит вызов какой-то стандартной suspend-функции из стандартной библиотеки корутин (в случае с примером ниже эта точка - метод delay());
* Если Job имеет потомков, то они тоже будут завершены;
* Когда Job будет завершена она больше не может быть использована для запуска новых корутин (состояние Cancelled).
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. Это исключение можно перехватить, но, чтобы избежать трудноуловимых багов, его лучше сразу выбросить снова:
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-функции, но не будет реагировать на сигнал завершения:
launch(job) {
try {
delay(200)
println("Coroutine finished")
} finally {
println("Finally")
withContext(NonCancellable) {
delay(1000L)
println("Cleanup done")
}
}
}
Еще один способ выполнить код после завершения корутины - это метод invokeOnCompletion(), который будет вызван независимо от того как была завершена корутина принудительно или она просто отработала свое время.
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 если корутина уже получила сигнал завершения.