В чём разница между cancelAndJoin() и cancel()?
cancel() лишь инициирует отмену и возвращается немедленно, тогда как cancelAndJoin() — suspend-функция, которая ждёт полного завершения Job включая finally-блоки. Используйте cancelAndJoin() перед перезапуском задачи или закрытием ресурсов.
Разница между cancel() и cancelAndJoin()
cancel() — функция на Job, которая инициирует отмену: выставляет флаг отмены и выбрасывает CancellationException при ближайшей точке остановки внутри корутины. Вызов не blocking и не ждёт завершения — он возвращается немедленно, пока корутина ещё может выполнять код в finally-блоке или cleanup-лямбдах.
cancelAndJoin() — suspend-функция, которая вызывает cancel() и затем join(). Она suspend-ится до тех пор, пока Job не перейдёт в состояние Cancelled, то есть пока все finally-блоки и cleanup-корутины не завершатся. Это гарантирует, что ресурсы освобождены до продолжения.
Пример: когда разница критична
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch(Dispatchers.IO) {
try {
repeat(10) { i ->
delay(200)
println("Working $i")
}
} finally {
// cleanup может занять время
withContext(NonCancellable) {
println("Cleanup start")
delay(500) // запись в БД, закрытие соединения
println("Cleanup done")
}
}
}
delay(500)
// Вариант 1: cancel() — не ждём cleanup
job.cancel()
println("After cancel — cleanup may still be running!") // выводится до "Cleanup done"
// Вариант 2: cancelAndJoin() — ждём полного завершения
// job.cancelAndJoin()
// println("After cancelAndJoin — cleanup is complete") // выводится после "Cleanup done"
println("Job state: ${job.isCompleted}")
}
Практическое применение
class DataRepository(
private val scope: CoroutineScope,
private val db: Database
) {
private var syncJob: Job? = null
suspend fun restartSync() {
// Неправильно: cancel() без join — старый job ещё пишет в БД
// syncJob?.cancel()
// Правильно: ждём завершения перед запуском нового
syncJob?.cancelAndJoin()
syncJob = scope.launch {
db.syncAll()
}
}
// В onDestroy / onCleared
suspend fun shutdown() {
syncJob?.cancelAndJoin() // гарантируем flush перед выходом
db.close()
}
}
Состояния Job после cancel/cancelAndJoin
- После
cancel(): Job в состоянииCancelling—isActive = false,isCancelled = true,isCompleted = false - После
cancelAndJoin(): Job в состоянииCancelled—isActive = false,isCancelled = true,isCompleted = true
val job = launch { delay(Long.MAX_VALUE) }
job.cancel()
println(job.isCompleted) // false — ещё в Cancelling
// vs
runBlocking {
job.cancelAndJoin()
println(job.isCompleted) // true — полностью завершён
}
Подводные камни
- Race condition после cancel() — запуск новой операции сразу после
cancel()может привести к параллельной работе старого и нового job-а - finally + suspend без NonCancellable — если в
finallyвызывать suspend-функции безwithContext(NonCancellable), они выбросятCancellationExceptionснова - cancelAndJoin в неотменяемом контексте — если сам вызывающий код в состоянии отмены,
cancelAndJoinтоже может быть прерван; используйтеwithContext(NonCancellable)при shutdown - Дедлок при cancel из того же потока — если корутина и вызывающий код на одном single-threaded dispatcher-е,
join()может не завершиться - SupervisorJob и отмена — отмена дочернего job не отменяет родителя при
SupervisorJob, ноcancelAndJoinна дочернем всё равно ждёт его cleanup - Игнорирование возвращаемого значения cancel() —
cancel()возвращаетBoolean, указывая, была ли отмена успешной; при уже завершённом job вернётfalse
Common mistakes
- Объяснять «cancelAndJoin() и cancel()» только как синтаксис и не описывать поведение runtime/compiler.
- Игнорировать важный риск: После cancel без join код может продолжить работу параллельно с finally-блоком отменяемой coroutine.
- Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.
What the interviewer is testing
- Формулирует суть темы «cancelAndJoin() и cancel()» своими словами и связывает ее с кодом.
- Называет механизм: Разница важна, когда после отмены нужно безопасно освободить ресурс или запустить следующую операцию.
- Видит production-последствие: После cancel без join код может продолжить работу параллельно с finally-блоком отменяемой coroutine.