Что такое 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.