Kotlin CoroutinesSeniorTechnical

Что такое structured concurrency и почему это важно?

Structured concurrency — гарантия, что дочерние корутины не переживают родительский scope; Job образует дерево, где отмена и ошибки распространяются по иерархии, предотвращая утечки.

Structured Concurrency в Kotlin Coroutines

Structured concurrency — принцип, при котором время жизни корутин ограничено явным scope. Корутина не может «вытечь» за пределы блока кода, в котором была запущена. Это делает конкурентный код предсказуемым: если scope завершён, все его дочерние корутины тоже завершены.

Иерархия Job

Каждая корутина имеет Job, который образует дерево. Родительский Job отслеживает все дочерние:

import kotlinx.coroutines.*

fun main() = runBlocking { // root scope
    val job = launch { // дочерний Job
        launch { // внук
            delay(1000)
            println("grandchild done")
        }
        println("child done")
    }
    // runBlocking ждёт ALL дочерних корутин
    println("parent done")
}
// Порядок: "child done" -> "grandchild done" -> "parent done"

coroutineScope — ключевой строительный блок

coroutineScope создаёт новый scope и приостанавливается до завершения всех дочерних корутин. При ошибке в любой дочерней — отменяет остальных и пробрасывает исключение:

suspend fun loadUserData(userId: String): UserData = coroutineScope {
    val profile = async { userRepo.getProfile(userId) }
    val orders = async { orderRepo.getOrders(userId) }
    val prefs = async { prefsRepo.getPrefs(userId) }

    // Если любой из них упадёт — все три будут отменены
    UserData(
        profile = profile.await(),
        orders = orders.await(),
        prefs = prefs.await()
    )
}

SupervisorScope для независимых задач

Когда ошибка одной задачи не должна отменять остальные — используем supervisorScope:

suspend fun loadDashboard(): Dashboard = supervisorScope {
    val news = async {
        try { newsApi.getLatest() }
        catch (e: Exception) { emptyList() } // деградирует gracefully
    }
    val stats = async { statsApi.getStats() } // падение не отменяет news

    Dashboard(news = news.await(), stats = stats.await())
}

Отмена и CancellationException

val scope = CoroutineScope(Job() + Dispatchers.IO)

val job = scope.launch {
    try {
        repeat(100) { i ->
            delay(100)
            println("Working $i")
        }
    } finally {
        // Cleanup всегда выполняется при отмене
        withContext(NonCancellable) {
            saveState() // cleanup может быть suspend
        }
    }
}

job.cancel() // немедленно отменяет всё поддерево
job.join()   // ждём завершения cleanup

Почему это важно

  • Без structured concurrency корутины могут жить дольше, чем нужно, удерживая память и соединения.
  • Гарантирует освобождение ресурсов (finally блоки) при отмене.
  • Упрощает рассуждение о коде: область жизни корутины — это лексическая область кода.
  • Ошибки всплывают корректно, не теряются в фоновых задачах.

Подводные камни

  • GlobalScope нарушает structured concurrency — корутины не привязаны ни к какому lifecycle.
  • CoroutineScope(Job()) как поле класса без cancel() в onCleared/onDestroy — ручная утечка.
  • async внутри GlobalScope: исключение не propagates в parent — теряется.
  • CancellationException, пойманный в catch(e: Exception) и не переброшенный, блокирует отмену.
  • withContext не создаёт новый Job — он использует Job текущей корутины, поэтому отмена родителя отменит его.
  • launch внутри suspend-функции (без receiver scope) требует GlobalScope или инжекции scope — архитектурный запах.
  • coroutineScope при ошибке отменяет ВСЕ дочерние — если нужна независимость, нужен supervisorScope.

Common mistakes

  • Объяснять «structured concurrency» только как синтаксис и не описывать поведение runtime/compiler.
  • Игнорировать важный риск: Неструктурированные jobs переживают запросы, экраны и тесты, поэтому production-система получает утечки и потерянную observability.
  • Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.

What the interviewer is testing

  • Формулирует суть темы «structured concurrency» своими словами и связывает ее с кодом.
  • Называет механизм: coroutineScope, supervisorScope и withContext задают разные границы распространения ошибки и cancellation.
  • Видит production-последствие: Неструктурированные jobs переживают запросы, экраны и тесты, поэтому production-система получает утечки и потерянную observability.

Sources

Related topics