Kotlin CoroutinesMiddleTechnical

В чём разница между 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 в состоянии CancellingisActive = false, isCancelled = true, isCompleted = false
  • После cancelAndJoin(): Job в состоянии CancelledisActive = 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.

Sources

Related topics